flutter自学笔记2-了解Flutter UI 页面和跳转

1,095 阅读12分钟

笔记1:介绍一下flutter

笔记2-了解Flutter UI 页面和跳转

笔记3- 常用 Widget 整理

笔记4- dart 语法快速学习

笔记5- dart 编码规范

笔记6- 网络请求、序列化、平台通道介绍

笔记7- 状态管理、数据持久化

笔记8- package、插件、主题、国际化

笔记9 - 架构、调试、打包部署

笔记10- Widget 构建、渲染流程和原理、布局算法优化

笔记11- 性能、渲染、包体积、懒加载、线程、并发隔离

通过上一篇《开篇:介绍一下flutter》, 我了解到 Flutter 大部分内容都是UI相关的库,打算先从UI 组件使用下手,这个看上去简单,如果直接学习dart语法会让我失去学习的兴趣。

一、笔记2:

正常学习一门语言会从 hello word 开始,这次我先不着急搭建环境,准备花点时间把Flutter UI组件大致熟悉一下

接下来,首先搜索一个中文版教程,我搜索“flutter 中文教程” 找到一个 “中文开发者网站” 的地址 (当然参考很多,比如 docs.flutter.dev/get-started… 看个人)

docs.flutter.cn/cookbook/ (2024.12可访问)

image.png

我进入 实用教程-动画-第一个示例页面,底部可以在线编译示例代码:

image.png

整体看上去基本满足我初学要求了,接下来我将快速学习 UI 组件的搭建示例了。

整体看下 docs.flutter.cn/cookbook/an… 示例。

二、实现“页面跳转”功能:

要求:一个主页路由,包含了 "Go!" 按钮,还有第二个路由,包含了一个显示 "Page 2 的标题

下面是flutter实现的代码示例:

1、由大模型添加注释

2、由于掘金对flutter的代码高亮效果几乎没有(2024/12),代码我使用js高亮

// 导入Flutter的Material Design组件库,这是构建用户界面所必需的。
import 'package:flutter/material.dart';
​
// main函数是Flutter应用的入口点。
void main() {
 // 使用runApp函数启动应用,参数是应用的根组件。
 runApp(
   // 使用const关键字创建MaterialApp的实例,这表示这个实例是不可变的。
   const MaterialApp(
     // home属性指定了应用启动时显示的第一个页面。
     homePage1(),
   ),
 );
}
​
// 定义一个名为Page1的类,它继承自StatelessWidget,表示这是一个无状态组件。
class Page1 extends StatelessWidget {
 // 使用const构造函数,允许在声明时直接创建实例,且实例不可变。
 const Page1({super.key});
​
 // build方法是每个Widget必须实现的,它描述了组件的UI结构。
 @override
 Widget build(BuildContext context) {
   // Scaffold是一个布局结构,通常用于包含AppBar和页面主体内容。
   return Scaffold(
     // appBar属性定义了应用栏。
     appBarAppBar(),
     // body属性定义了Scaffold的主体内容。
     bodyCenter(
       // Center组件用于将其子组件居中显示。
       childElevatedButton(
         // onPressed属性定义了按钮被点击时的行为。
         onPressed: () {
           // Navigator用于管理路由(页面跳转)。push方法用于将新页面添加到路由栈中。
           Navigator.of(context).push(_createRoute());
         },
         // child属性定义了按钮的内容。
         childconst Text('Go!'),
       ),
     ),
   );
 }
}
​
// _createRoute是一个私有方法,用于创建并返回一个Route对象。
Route _createRoute() {
 // PageRouteBuilder是一个灵活的页面路由构建器,允许自定义页面转换动画和页面构建逻辑。
 return PageRouteBuilder(
   // pageBuilder属性定义了如何构建新页面。
   pageBuilder(context, animation, secondaryAnimation) => const Page2(),
   // transitionsBuilder属性定义了页面转换动画。这里我们直接返回child,表示没有特殊动画。
   transitionsBuilder: (context, animation, secondaryAnimation, child) {
     return child;
   },
 );
}
​
// 定义了一个名为Page2的类,同样继承自StatelessWidget。
class Page2 extends StatelessWidget {
 // 使用const构造函数。
 const Page2({super.key});
​
 // build方法定义了Page2的UI结构。
 @override
 Widget build(BuildContext context) {
   // 返回Scaffold组件,其中包含一个AppBar和一个居中的文本“Page 2”。
   return Scaffold(
     appBarAppBar(),
     bodyconst Center(
       // 使用const因为Text组件的内容在构建后不会改变。
       childText('Page 2'),
     ),
   );
 }
}

整体看下看作为UI 组件的代码逻辑,各个平台都大同小异,因为本质上都是UI功能的封装,

贴一个SwiftUI 的类似功能的示例代码:

在 SwiftUI 中实现页面切换并加入动画效果,你可以使用 NavigationViewNavigationLinkNavigationStack(iOS 16+)来管理导航,并使用 withAnimationtransition 来添加动画效果。下面是一个简单的例子,展示了一个包含 "Go!" 按钮的主页路由,以及一个显示 "Page 2 的标题" 的第二个路由,并在页面切换时添加动画效果。

import SwiftUI
​
struct ContentViewView {
   @State private var isPresentingSecondPage = false
​
   var body: some View {
       NavigationStack {
           HomePage(isPresentingSecondPage: $isPresentingSecondPage)
               .navigationDestination(for: IsPresentingSecondPage.self) { isPresentingin
                   if isPresenting {
                       SecondPage()
                           .transition(.move(along: .horizontal))
                           .animation(.spring())
                   }
               }
       }
   }
}
​
enum IsPresentingSecondPageHashable {
   case presenting
   case notPresenting
}
​
struct HomePageView {
   @Binding var isPresentingSecondPage: Bool
​
   var body: some View {
       VStack {
           Text("Home Page")
               .font(.largeTitle)
               .padding()
           
           Button("Go!") {
               withAnimation(.spring()) {
                   isPresentingSecondPage = true
               }
           }
           .padding()
           .background(Color.blue)
           .foregroundColor(.white)
           .cornerRadius(10)
       }
   }
}
​
struct SecondPageView {
   var body: some View {
       VStack {
           Text("Page 2 的标题")
               .font(.largeTitle)
               .padding()
           
           // 添加一个返回按钮(如果需要)
           Button("Go Back") {
               NavigationStack.pop()
           }
           .padding()
           .background(Color.green)
           .foregroundColor(.white)
           .cornerRadius(10)
       }
   }
}
​
@main
struct MyAppApp {
   var body: some Scene {
       WindowGroup {
           ContentView()
       }
   }
}

同样,我再使用鸿蒙实现上述push页面的功能:

在鸿蒙(HarmonyOS)开发中,实现页面切换并加入动画效果,你可以使用ArkUI框架中的页面导航组件和动画API。以下是一个简单的示例,展示了一个包含"Go!"按钮的主页路由,以及一个显示"Page 2 的标题"的第二个路由,并在页面切换时添加动画效果。

主页路由(HomePage.ets)

@Entry
@Component
struct HomePage {
  @State goToPage2: boolean = false

  build() {
      Column() {
          Text('Home Page')
              .fontSize(24)
              .textAlign(TextAlign.Center)
              .padding(20)

          Button('Go!')
              .onClick(() => {
                  this.goToPage2 = true
                  router.push({
                      uri: 'pages/Page2/Page2',
                      params: {
                          // 可以传递参数,如果需要的话
                      },
                      options: {
                          animation: {
                              type: AnimationType.Slide,
                              direction: AnimationDirection.RightToLeft,
                              duration: 300 // 动画持续时间,单位为毫秒
                          }
                      }
                  })
              })
              .padding(20)
              .margin({ top: '20vp' })
      }
  }
}

第二个路由(Page2.ets)

@Entry
@Component
struct Page2 {
  build() {
      Column() {
          Text('Page 2 的标题')
              .fontSize(24)
              .textAlign(TextAlign.Center)
              .padding(20)

          // 你可以在这里添加返回按钮或其他UI元素
          // 注意:鸿蒙系统通常会在页面顶部自动添加一个返回按钮(如果适用)
      }
  }
}

// 如果需要处理从主页传递过来的参数,可以在这里添加逻辑
// 例如,通过this.$route.params访问参数

以上SwiftUI 和 鸿蒙ArkUI 示例仅供我对比,不进行解释,通过以上UI 对比,我对Flutter UI学习心里更有底了。

继续回到开头Flutter 代码示例:

三、了解 Flutter Widget

Flutter Widget是Flutter框架中用于构建用户界面的基本构建模块。以下是对Flutter Widget搭建UI的详细介绍:

1、Flutter Widget的基本概念

  1. 定义:Widget是Flutter中定义和构建用户界面的基本单元,相当于其他UI框架中的“控件”或“组件”。无论是简单的文本、按钮、图标,还是复杂的布局、列表、滑动容器,甚至包括动画效果、手势处理、主题样式等,一切都是以Widget的形式来实现和组织的。

  2. 特性

    • 不可变性:Widget是不可变的,即它们的属性在创建后不能更改。如果需要更新Widget,则需要创建一个新的Widget实例。
    • 轻量级:Widget是轻量级的,它们只是描述了UI的结构和外观,而不包含任何状态或行为。
    • 组合性:Widget可以通过嵌套和组合来创建更复杂的UI组件。

2、Flutter Widget的类型

Flutter中的Widget主要分为两种类型:StatelessWidget和StatefulWidget。

  1. StatelessWidget:无状态Widget,适用于不需要维护内部状态或不随时间变化的UI元素。一旦创建,其属性和外观就不会改变。
  2. StatefulWidget:有状态Widget,用于需要维护内部状态并在状态变化时触发界面更新的场景。例如,一个计数器组件,其计数值随着用户的点击操作而增加。

3、Flutter Widget的层次结构

Widget之间可以通过嵌套形成树状结构,反映UI的层级关系。父Widget可以包含多个子Widget,而每个子Widget又可以有自己的子Widget。这种结构使得布局、样式传递以及事件响应能够沿着树形结构进行管理和传递。

4、Flutter Widget的构建过程

  1. 定义Widget:通过定义一个或多个Widget类来描述UI的结构和外观。
  2. 构建Widget树:在应用的根Widget(通常由runApp函数指定)中,通过嵌套和组合其他Widget来构建完整的Widget树。
  3. 渲染Widget树:Flutter框架会遍历Widget树,并根据每个Widget的描述来渲染UI。当Widget的状态发生变化时,Flutter框架会自动计算出差异并更新界面,无需手动管理界面刷新过程。

5、Flutter Widget的常用组件

Flutter提供了许多内置的Widget,用于构建各种不同的界面元素。以下是一些常用的Widget:

  1. 页面元素

    • runApp 函数是Flutter应用的入口点。它接受一个Widget作为参数,并将这个Widget设置为整个Widget树的根。

      例如:void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized().attachRootWidget(app).scheduleWarmUpFrame(); }

    • Widget 是Flutter中用于描述用户界面的基本构建块。每个Widget都描述了界面的一部分,并可以包含其他Widget,是不可变的,当它们的属性发生变化时,Flutter会创建新的Widget来替换旧的

    • build 方法是每个Widget类必须实现的方法。它返回一个Widget,描述了该Widget的当前状态。

      在Flutter中,Widget树是通过递归调用每个Widgetbuild方法来构建的

    • StatelessWidget 是一个基础类,用于构建不可变的用户界面。

      一旦创建,StatelessWidget的状态就不会再改变。因此,它们通常用于展示静态内容,如文本、图像或图标。

      例如:TextIconImageIconDialog等都是常见的StatelessWidget

    • StatefulWidgetStatelessWidget不同,StatefulWidget是可变的。它们的状态可以在运行时改变,并触发界面的重新构建。

      StatefulWidget通过实现State<T>类来管理其状态。当状态发生变化时,需要调用setState方法来通知Flutter框架,以便重新构建界面。

      例如:CheckboxRadioSliderInkWellFormTextField等都是常见的StatefulWidget

    • body 和child bodychild是Flutter中一些布局Widget的属性,用于指定其子Widget

      例如,在Scaffold中,body属性用于指定页面的主要内容。而在一些容器类Widget中,如ContainerColumnRow等,childchildren属性用于指定其子Widget

    • onPressed onPressed是一个回调函数属性,通常用于处理按钮点击事件。

      当用户点击按钮时,onPressed会被调用,并可以执行一些操作,如打开对话框、导航到另一个页面等

    • Navigator Navigator是Flutter中用于实现页面导航的组件。它管理着一个路由栈,允许用户在应用的不同页面之间切换。

      Navigator提供了多种方法来实现页面跳转,如pushpushReplacementpop等。

      通过命名路由或路由表,可以更方便地在应用的不同部分之间导航。

  2. 基础Widget

    • Text:用于显示文本。
    • Image:用于显示图片。
    • Icon:用于显示图标,通常与IconData类一起使用来指定要显示的图标。
  3. 容器组件(Container Widgets)

    • Container:一个强大的容器组件,可以包含其他组件,并提供了一系列属性来控制其外观和布局,如背景色、边框、阴影、内外边距等。
    • Padding和Margin:这两个组件分别用于在组件内部和外部添加空白区域,以控制其子组件或相邻组件之间的间距。
    • Decoration:用于设置组件的装饰效果,如边框、圆角、阴影等。
  4. 布局Widget

    • Row:用于在水平方向上排列子元素。
    • Column:用于在垂直方向上排列子元素。
    • Stack:允许子Widget堆叠,允许子组件在Z轴上重叠。
    • GridView和ListView:用于展示列表或网格布局,其中ListView可以垂直或水平滚动,而GridView则创建一个二维网格。
    • Flexible和ExpandedFlexible用于容纳多行文本或自适应高度的子组件,而Expanded则使组件尽可能地占据剩余的空间。
  5. 交互Widget

    • Button:用于创建按钮,可以响应用户的点击操作。
    • CheckBoxRadioButton:用于创建复选框和单选按钮。
    • Slider:用于创建滑动条,可以响应用户的拖动操作。
  6. 导航Widget

    • Navigator:用于管理页面导航和路由。
    • TabBarTabBarView:用于创建底部导航栏和与之关联的视图。

6、Flutter Widget的状态管理

状态管理是Flutter应用中的一个重要概念,它帮助开发者有效地管理应用程序的数据和UI状态,以便实现复杂的交互和数据流。在Flutter中,有多种状态管理的方法可供选择,每种方法都有其适用的场景和优势。

  1. 局部状态管理:通过StatefulWidget和setState方法来实现。适用于小规模的Widget和临时性状态管理。
  2. 全局状态管理:通过Provider、Redux等状态管理库来实现。适用于大规模应用和需要跨组件共享状态的情况。

小结:通过不断修改示例代码,观察页面变化来熟悉以上概念

四、实现“列表” 功能

4.1、展示一个基础列表

从页面只有一个按钮“go” 改为 显示一个基础列表 使用flutter标准的 ListView 构造方法非常适合只有少量数据的列表。我们还将使用内置的 ListTile widget 来给我们的条目提供可视化结构。

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const title = '基础列表';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: ListView(
          children: const <Widget>[
            ListTile(
              leading: Icon(Icons.map),
              title: Text('地图'),
            ),
            ListTile(
              leading: Icon(Icons.photo_album),
              title: Text('相册'),
            ),
            ListTile(
              leading: Icon(Icons.phone),
              title: Text('电话'),
            ),
          ],
        ),
      ),
    );
  }
}
运行效果

在线(docs.flutter.cn)运行,效果如下:

image.png

4.2、展示一个“横向”列表

在实用教程-创建一个水平滑动的列表 示例:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const title = '水平滑动的列表';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: Container(
          margin: const EdgeInsets.symmetric(vertical: 20),
          height: 100,
          child: ListView(
            // This next line does the trick.
            scrollDirection: Axis.horizontal,
            children: <Widget>[
              Container(
                width: 100,
                color: Colors.red,
              ),
              Container(
                width: 60,
                color: Colors.orange,
              ),
              Container(
                width: 90,
                color: Colors.yellow,
              ),
              Container(
                width: 100,
                color: Colors.green,
              ),
              Container(
                width: 160,
                color: Colors.orange,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
运行效果

在线(docs.flutter.cn)运行,效果如下:

image.png

4.3、展示一个“网格”列表

参考 创建一个网格列表 我展示了一个九宫格:

import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const title = '九宫格';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: GridView.builder(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            crossAxisSpacing: 5.0,  // 横向间距
            mainAxisSpacing: 5.0,   // 纵向间距
          ),
          itemCount: 9,
          itemBuilder: (BuildContext context, int index) {
            Color randomColor = Color(
              (math.Random().nextInt(0xFFFFFFFF) << 8) + 0xFF000088
            ).withOpacity(1.0);  // 生成随机颜色

            return Container(
              color: randomColor,
              child: Center(
                child: Text(
                  'Item $index',
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
              ),
              margin: const EdgeInsets.all(5.0),  // 外边距
            );
          },
        ),
      ),
    );
  }
}
运行效果

在线(docs.flutter.cn)运行,效果如下:

image.png

还有更多,这里不列举了。

部分概念由文心参考网络生成并整理

五、实现“Tab”功能

在Flutter中,AppBar、tabs(通常通过TabBar实现)、Scaffold、TabController和DefaultTabController是用于构建具有多页面导航功能的应用界面的关键组件。以下是对这些组件的详细介绍:

5.1. tab页一些概念

1. Scaffold
  • 作用:Scaffold是Flutter中用于构建页面骨架的组件,它提供了标准的Material Design布局结构,包括标题栏(AppBar)、主体内容区域、底部导航栏等。

  • 常用属性

    • appBar:用于设置页面的顶部导航栏(AppBar)。
    • body:页面的主体内容区域。
    • bottomNavigationBar:底部的导航栏。
    • drawer:侧拉导航菜单。
2. AppBar
  • 作用:AppBar是Scaffold组件中用于设置顶部导航栏的组件。

  • 常用属性

    • title:导航栏的标题,通常显示为Text组件,但也可以是其他Widget。
    • backgroundColor:导航栏的背景颜色。
    • leading:导航栏最左侧的组件,通常是返回按钮或应用的logo。
    • actions:导航栏右侧的组件组,通常用于放置IconButton。
    • bottom:可以设置为TabBar,以实现顶部标签页的切换。
    • isScrollable:顶部TabBar是否可以滚动。
3. TabBar
  • 作用:TabBar是用于显示一行标签(Tab)的组件,用户可以通过点击标签来切换不同的页面或视图。

  • 常用属性

    • tabs:一个包含多个Tab对象的列表,用于定义标签页的内容。
    • controller:一个TabController对象,用于控制标签页的切换。
    • isScrollable:是否允许标签页滚动。
    • indicator及相关属性:用于自定义标签页选中时的指示器样式。
4. TabBarView
  • 作用:TabBarView是与TabBar配合使用的组件,用于显示当前选中的标签页对应的内容。

  • 常用属性

    • children:一个Widget列表,每个Widget对应一个标签页的内容。
    • controller:与TabBar共用的TabController对象,用于同步标签页的切换。
5. TabController
  • 作用:TabController是一个控制器对象,用于在TabBar和TabBarView之间协调标签页的选择。

  • 使用场景

    • 当需要动态添加或删除标签页时。
    • 当需要监听标签页的变化时。
    • 在更复杂的功能场景中,通常需要手动创建TabController。
6. DefaultTabController
  • 作用:DefaultTabController是一个简化的TabController,它通常用于快速构建具有固定数量标签页的页面。

  • 特点

    • 它不需要手动创建和释放TabController。
    • 它将TabBar和TabBarView封装在一起,简化了标签页的实现。
    • 它适用于功能不复杂的场景。

5.2 综合示例

以下是一个综合使用这些组件的示例代码:

import 'package:flutter/material.dart';
 
void main() {
  runApp(MyApp());
}
 
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DefaultTabController(
        length: 3, // 标签页的数量
        child: Scaffold(
          appBar: AppBar(
            title: Text('AppBar Tabs Demo'),
            bottom: TabBar(
              tabs: [
                Tab(icon: Icon(Icons.home), text: 'Home'),
                Tab(icon: Icon(Icons.search), text: 'Search'),
                Tab(icon: Icon(Icons.settings), text: 'Settings'),
              ],
            ),
          ),
          body: TabBarView(
            children: [
              Center(child: Text('Home Page')),
              Center(child: Text('Search Page')),
              Center(child: Text('Settings Page')),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中,我们使用了DefaultTabController来快速构建了一个具有三个标签页的页面。每个标签页都对应一个TabBar中的标签和一个TabBarView中的内容。用户可以通过点击TabBar中的标签来切换不同的页面或视图。

六、实现“导航/路由”功能

前面实现了基础列表,现在完善点击跳转到详情页的功能,这涉及到“导航/路由”功能

在Flutter中,导航和路由功能是实现页面之间跳转和管理的关键机制。以下是对Flutter导航和路由功能的详细介绍,以及一个具体的例子。

6.1 Flutter导航和路由功能介绍

1. 路由(Route)
  • 定义:在Flutter中,路由(Route)通常指页面(Page),它是页面的抽象表示。路由管理和导航时,会根据特定的路由名或路由对象来决定跳转到哪个页面。

  • 类型

    • 命名路由:有名字的路由,可以通过路由名字直接打开新的路由,为路由管理带来了一种直观、简单的方式。使用命名路由时,需要在应用程序的路由表中注册路由名称和对应的页面组件。
    • 普通路由:直接指定要跳转的页面组件的方式,而不使用路由名称。
2. 导航(Navigation)
  • 定义:导航是从一个页面跳转到另一个页面的过程。

  • 核心组件:Navigator是Flutter提供的内置路由管理器,它使用栈(stack)的方式来管理页面的进出。栈模型页面导航类似于入栈(push)和出栈(pop)的操作,最新的页面会被推入栈顶,点击返回按钮时,页面从栈顶弹出,回到之前的页面。

  • 常用方法

    • Navigator.push:将一个新的页面压入栈中,显示该页面。
    • Navigator.pop:将当前页面从栈中弹出,返回到上一个页面。
    • Navigator.pushReplacement:将当前页面替换为一个新的页面,并清除当前页面。
    • Navigator.pushNamedNavigator.popNamed:使用命名路由进行页面导航。
    • Navigator.pushAndRemoveUntil:跳转到新页面,并移除直到满足条件的所有页面。
    • Navigator.maybePop:尝试弹出当前页面,如果成功则返回true,否则返回false。

6.2 简单示例:导航和路由实现

以下是一个简单的Flutter应用示例,展示了如何使用Navigator进行页面跳转和管理路由。

1.代码结构
  • main.dart:应用的主入口文件,通常包含MaterialApp和初始路由设置。

  • screens/:存放应用的各个页面(Screen),每个页面通常作为一个单独的Dart文件进行管理。

    • first_page.dart:第一个页面。
    • second_page.dart:第二个页面。
2.代码实现

main.dart

import 'package:flutter/material.dart'; // 导入Flutter的Material Design组件库
import 'screens/first_page.dart'; // 导入FirstPage页面
import 'screens/second_page.dart'; // 导入SecondPage页面
 
 void main() => runApp(MyApp());
 
// MyApp类,它继承自StatelessWidget,表示这个组件没有需要管理的状态
class MyApp extends StatelessWidget {
  // build方法是StatelessWidget类必须实现的方法,它返回一个Widget,即这个组件的UI描述
  @override
  Widget build(BuildContext context) {
    // 返回MaterialApp组件,它是Flutter应用的基础组件之一,提供了Material Design风格的UI
    return MaterialApp(
      // initialRoute属性设置应用的初始路由,即应用启动时首先显示的页面
      initialRoute: '/',
      // routes属性是一个Map,它定义了应用中所有可能的路由及其对应的组件
      // 当Navigator接收到一个路由请求时,它会在这个Map中查找对应的组件并显示
      routes: {
        // '/'路由对应的组件是FirstPage
        '/': (context) => FirstPage(),
        // '/second'路由对应的组件是SecondPage
        '/second': (context) => SecondPage(),
      },
      // 注意:这里省略了其他可能的MaterialApp属性,如theme、home等
      // 因为在这个例子中,我们使用了routes属性来定义所有路由,所以不需要home属性
    );
  }
}

first_page.dart

import 'package:flutter/material.dart';
 
class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(context, '/second');
          },
          child: Text('Go to Second Page'),
        ),
      ),
    );
  }
}

second_page.dart

import 'package:flutter/material.dart';
 
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back to First Page'),
        ),
      ),
    );
  }
}
3.功能说明
  1. main.dart

    • 定义了应用的入口MyApp,它是一个StatelessWidget
    • MaterialApp中设置了初始路由'/',并定义了路由表,将'/'映射到FirstPage,将'/second'映射到SecondPage
  2. first_page.dart

    • 定义了第一个页面FirstPage,它是一个StatelessWidget
    • Scaffoldbody中放置了一个按钮,当按钮被点击时,使用Navigator.pushNamed方法跳转到'/second'路由,即SecondPage
  3. second_page.dart

    • 定义了第二个页面SecondPage,它是一个StatelessWidget
    • Scaffoldbody中放置了一个按钮,当按钮被点击时,使用Navigator.pop方法返回上一个页面,即FirstPage

通过上述代码,我们实现了一个简单的Flutter应用,其中包含了两个页面,并能够通过按钮点击在它们之间进行导航和路由跳转。

至此我大致了解了实现一个简单tab页,以及页面间跳转

6.3 路由设计

在Flutter中,实现最优的路由方案需要考虑多个因素,包括性能、灵活性、可维护性以及用户体验。以下是一些建议,可以帮助你设计出高效的Flutter路由方案:

1. 使用Flutter内置的路由系统

Flutter提供了内置的路由系统,通过NavigatorRoute这两个核心概念来管理页面导航。Navigator是一个堆栈管理器,用于管理应用中的路由栈,而Route则表示导航堆栈中的一个页面。

  • 基本路由:你可以使用Navigator.pushNavigator.pop等方法在路由栈上进行页面的添加和移除。
  • 命名路由:通过给路由起一个名字,你可以使用Navigator.pushNamedNavigator.popNamed等方法进行页面跳转,这种方式使得页面跳转更为直观和易于管理。
2. 路由表的设计

为了管理多个路由,你可以定义一个路由表(routing table),它是一个Map,将路由名称映射到对应的组件构建函数。这样,你就可以通过路由名称来查找和构建对应的页面组件。

3. 参数传递与状态管理
  • 参数传递:在Flutter路由中,你可以通过路由参数来传递数据。例如,在命名路由中,你可以使用arguments参数来传递数据到目标页面。
  • 状态管理:对于复杂的应用,你可能需要使用状态管理库(如Provider、Riverpod、MobX等)来管理全局状态。这些库可以帮助你在不同的页面和组件之间共享和更新状态。
4. 路由守卫与权限管理

在某些情况下,你可能需要在页面跳转之前进行权限检查或数据验证。为此,你可以实现路由守卫(route guards),在路由跳转之前执行一些逻辑来判断是否允许跳转。

5. 深度链接与全局导航

深度链接(deep linking)允许用户从应用的外部链接直接跳转到应用内的特定页面。为了实现深度链接,你需要在路由表中定义与URL路径对应的路由,并在应用启动时解析URL以进行页面跳转。

全局导航则涉及到在整个应用中维护一致的导航体验。你可以通过定义统一的导航条、页面切换动画等方式来提升用户体验。

6. 多引擎路由方案(针对复杂应用)

对于特别复杂的应用,特别是那些需要在Flutter页面和原生页面之间频繁切换的应用,你可以考虑使用多引擎路由方案。这种方案允许你为每个页面或模块创建独立的Flutter引擎实例,从而实现更灵活的路由管理和状态隔离。然而,需要注意的是,多引擎方案可能会增加应用的资源消耗和复杂性。

7. 路由解析与动态路由

在处理外部传入的URI时,你需要进行路由解析以获取路由名称和参数。Flutter提供了强大的路由解析工具,你可以使用这些工具来解析URI并执行相应的页面跳转。此外,你还可以实现动态路由,根据用户的操作或数据变化来动态地添加或删除路由。

8. 路由表的注册

//注册路由表
routes:{
  "new_page":(context) => EchoRoute(),
},

在路由⻚通过 RouteSetting 对象获取路由参数:


class EchoRoute extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        //获取路由参数
        var args=ModalRoute.of(context).settings.arguments; //...省略无关代码
    } 
    
}
    
      

在打开路由时传递参数


Navigator.of(context).pushNamed("new_page", arguments: "hi");

适配 假设我们也想将上面路由传参示例中的 TipRoute 路由⻚注册到路由表中,以便也可以通过路由名来打开它。 但是,由于TipRoute接受一个text 参数,在不改变TipRoute源码的前提下适配这种情况:


routes: {
   "tip2": (context){
     return TipRoute(text: ModalRoute.of(context).settings.arguments);
    }, 
},

9. 路由生成钩子

背景

在开发电商APP时,我们希望实现以下功能:

  • 用户未登录时,可以查看店铺、商品等公开信息。
  • 用户需要登录后才能查看交易记录、购物车、个人信息等隐私页面。

问题

每次打开路由前判断用户登录状态会非常繁琐。

解决方案

使用 MaterialApp 的 onGenerateRoute 属性,在打开命名路由时进行统一的权限控制。

原理

  • onGenerateRoute 回调在尝试打开命名路由时可能会被调用。
  • 如果指定的路由名在路由表中已注册,则调用路由表中的 builder 函数生成路由组件。
  • 如果路由表中没有注册,才会调用 onGenerateRoute 来生成路由。

回调签名

Route<dynamic> Function(RouteSettings settings)

实现步骤

  1. 放弃使用路由表,转而使用 onGenerateRoute 回调。
  2. 在 onGenerateRoute 回调中进行统一的权限控制。

示例代码


MaterialApp(
    ... // 省略无关代码
    onGenerateRoute: (RouteSettings settings) {
        return MaterialPageRoute(
            builder: (context) {
                String routeName = settings.name;
 
                // 判断当前路由是否需要登录权限
                if (/* 路由需要登录且当前未登录 */) {
                    // 返回登录页面路由,引导用户登录
                    return LoginPage();
                } else {
                    // 根据路由名返回相应的页面
                    if (routeName == 'home') {
                        return HomePage();
                    } else if (routeName == 'product') {
                        return ProductPage();
                    } else if (routeName == 'user_info') {
                        return UserInfoPage();
                    }
                    // 可以继续添加其他路由判断
 
                    // 默认返回未找到的页面(可选)
                    return NotFoundPage();
                }
            }
        );
    }
);

注意事项

  • onGenerateRoute 只会对命名路由生效。
  • 在实际项目中,需要完善权限判断逻辑,例如通过用户状态管理(如 ProviderRiverpod 等)来判断用户是否登录。

通过这种方式,我们能够在不增加每个路由判断逻辑的情况下,实现统一的页面权限控制,提高代码的可维护性和可读性

10. 第三方路由方案参考

在Flutter应用开发中,除了使用Flutter官方提供的路由解决方案外,还可以选择多种优秀的第三方路由方案来满足不同项目的需求。以下是一些值得推荐的Flutter第三方路由方案:

1. go_router
  • 特点

    • 提供了灵活的路由定义和管理方式,支持URL格式的自定义和URL跳转。
    • 可以处理深度链接,适用于Web和App应用。
    • 支持模板语法解析路由路径和查询参数。
    • 支持单个目标路由展示多个页面(子路由)。
    • 提供重定向功能,可以基于应用状态跳转到不同的URL。
    • 支持嵌套的Tab导航(使用StatefulShellRoute)。
    • 兼容Navigator API,易于集成和迁移。
  • 使用

    • pubspec.yaml文件中添加go_router依赖。
    • 在应用中配置GoRouter,包括定义初始位置、路由等。
    • 使用MaterialApp.routerCupertinoApp.router构造函数,将GoRouter配置对象传递给routerConfig参数。
    • 通过GoRoute对象配置路由参数,包括路径参数和查询参数。
    • 使用context.go()context.goNamed()方法进行页面跳转。
2. Fluro
  • 特点

    • 层次分明、条理化,方便扩展和整体管理路由。
    • 适合中大型项目,提供了清晰的路由配置和跳转方式。
    • 使用RouteTree存储已定义的路由,通过define方法注册路由。
    • 提供了navigateTo方法进行页面跳转,支持自定义过渡效果。
  • 使用

    • pubspec.yaml文件中添加Fluro依赖。
    • 初始化Router对象。
    • 使用define方法注册路由,包括路由路径和处理器(Handler)。
    • 在需要跳转的地方使用Application.router.navigateTo()方法进行跳转。
3. GetX(包含路由管理功能)
  • 特点

    • 是一个快速、轻量级的状态管理和路由管理库。
    • 提供了依赖注入、路由管理、国际化、主题切换等便利功能。
    • 语法简洁,性能优秀,适合构建中小型应用。
    • 路由管理部分与状态管理部分紧密集成,提供了便捷的状态和路由管理体验。
  • 使用

    • pubspec.yaml文件中添加get依赖。
    • 使用Get.to()Get.toNamed()方法进行页面跳转。
    • 可以通过Get.find<>()方法获取状态管理对象,实现状态共享和管理。
4. BeeRouter
  • 特点

    • 提供了简洁明了的路由配置方式。
    • 支持路径参数和查询参数的传递。
    • 支持嵌套路由和子路由。
    • 提供了全局和局部的路由守卫功能,可以在路由跳转前后执行特定逻辑。
  • 使用

    • 需要在pubspec.yaml文件中添加BeeRouter的依赖。
    • 在应用中配置BeeRouter,包括定义路由、设置初始页面等。
    • 使用BeeRouter提供的API进行页面跳转和参数传递。
5. VRouter
  • 特点

    • 提供了强大的路由管理功能,包括嵌套路由、路径参数、查询参数等。
    • 支持动态路由和静态路由的混合使用。
    • 提供了全局和局部的导航守卫,可以在导航前后执行自定义逻辑。
    • 支持多语言路由,可以根据语言动态调整路由路径。
  • 使用

    • pubspec.yaml文件中添加VRouter的依赖。
    • 配置VRouter,包括定义路由、设置导航守卫等。
    • 使用VRouter提供的API进行页面跳转和参数传递。
6. Beamer
  • 特点

    • 提供了基于URL的声明式路由管理。
    • 支持路径参数、查询参数和嵌套路由。
    • 提供了路由状态管理功能,可以方便地跟踪当前路由和导航历史。
    • 提供了强大的错误处理和恢复机制,可以在路由错误时执行自定义逻辑。
  • 使用

    • pubspec.yaml文件中添加Beamer的依赖。
    • 配置Beamer,包括定义路由、设置状态管理等。
    • 使用Beamer提供的API进行页面跳转和参数传递。

七、页面之间传值

在Flutter中,页面之间传值有多种方法,每种方法都有其适用的场景和优缺点。以下是一些常用的传值方法及其推荐理由:

7.1. 构造函数参数传递

  • 实现方式:在目标页面的构造函数中定义参数,并在导航到该页面时将值传递给构造函数。

  • 优点

    • 直观易懂,符合面向对象编程的常规做法。
    • 适用于传递简单的数据类型,如字符串、数字等。
  • 缺点

    • 当需要传递的数据较多或类型复杂时,构造函数可能会变得臃肿。
  • 代码示例

假设我们有两个页面,FirstPageSecondPage。我们希望在FirstPage中点击一个按钮后,跳转到SecondPage并传递一个字符串值。

// FirstPage.dart
import 'package:flutter/material.dart';
import 'SecondPage.dart';
 
class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => SecondPage(value: 'Hello from FirstPage!'),
              ),
            );
          },
          child: Text('Go to Second Page'),
        ),
      ),
    );
  }
}
 
// SecondPage.dart
import 'package:flutter/material.dart';
 
class SecondPage extends StatelessWidget {
  final String value;
 
  SecondPage({required this.value});
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Center(
        child: Text(value),
      ),
    );
  }
}

7.2. 路由参数传递

  • 实现方式:使用Navigator.pushNavigator.pushNamed方法时,在RouteSettingsarguments参数中传递数据。在目标页面中,使用ModalRoute.of(context).settings.arguments来获取传递的参数。

  • 优点

    • 可以在不修改目标页面构造函数的情况下传递数据。
    • 适用于传递任意类型的对象。
  • 缺点

    • 需要手动从ModalRoute中提取参数,可能会增加一些样板代码。
  • 代码示例

在这个例子中,我们同样使用FirstPageSecondPage,但这次我们使用命名路由和路由参数来传递值。

// main.dart
import 'package:flutter/material.dart';
import 'FirstPage.dart';
import 'SecondPage.dart';
 
void main() {
  runApp(MaterialApp(
    initialRoute: '/',
    routes: {
      '/': (context) => FirstPage(),
      '/second': (context) => SecondPage(),
    },
  ));
}
 
// FirstPage.dart
import 'package:flutter/material.dart';
 
class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/second',
              arguments: 'Hello from FirstPage with Named Route!',
            );
          },
          child: Text('Go to Second Page with Named Route'),
        ),
      ),
    );
  }
}
 
// SecondPage.dart
import 'package:flutter/material.dart';
 
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final String value = ModalRoute.of(context)!.settings.arguments as String;
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Center(
        child: Text(value),
      ),
    );
  }
}

7.3. 状态管理工具

  • 实现方式:使用Flutter的状态管理工具(如Provider、GetX、Riverpod等)来共享和传递值。这些工具可以在应用程序的不同页面之间共享状态,并在需要时更新值。

  • 优点

    • 实现了全局状态管理,方便在不同页面之间共享数据。
    • 支持数据的实时更新和同步。
  • 缺点

    • 需要学习并掌握状态管理工具的用法。
    • 可能会增加项目的复杂性和代码量。
  • 代码示例

在这个例子中,我们将使用Provider库来管理全局状态,并在页面之间共享这个状态。

// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'FirstPage.dart';
import 'SecondPage.dart';
import 'my_provider.dart'; // 假设我们在这里定义了MyProvider
 
void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<MyProvider>(create: (_) => MyProvider()),
      ],
      child: MaterialApp(
        initialRoute: '/',
        routes: {
          '/': (context) => FirstPage(),
          '/second': (context) => SecondPage(),
        },
      ),
    ),
  );
}
 
// my_provider.dart
import 'package:flutter/material.dart';
 
class MyData {
  String value = 'Initial Value';
}
 
class MyProvider with ChangeNotifier {
  MyData _data = MyData();
 
  MyData get data => _data;
 
  void setValue(String newValue) {
    _data.value = newValue;
    notifyListeners();
  }
}
 
// FirstPage.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
 
class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final myProvider = Provider.of<MyProvider>(context);
 
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Current Value: ${myProvider.data.value}'),
            ElevatedButton(
              onPressed: () {
                myProvider.setValue('New Value from FirstPage');
                Navigator.pushNamed(context, '/second');
              },
              child: Text('Update Value and Go to Second Page'),
            ),
          ],
        ),
      ),
    );
  }
}
 
// SecondPage.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
 
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final myProvider = Provider.of<MyProvider>(context);
 
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Center(
        child: Text('Current Value: ${myProvider.data.value}'),
      ),
    );
  }
}
  • 代码示例: 使用 Riverpod 状态管理

Riverpod 是 Provider 的一个更现代、更灵活的替代品,它提供了更好的依赖注入和响应式状态管理。

import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final myProvider = StateProvider<String>((ref) {
  return "Initial Value";
});
 
// 在 MaterialApp 外部创建 ProviderContainer
void main() {
  runApp(
    ProviderScope(
      overrides: [myProvider.overrideWithValue("Overridden Initial Value")],
      child: MyApp(),
    ),
  );
}
 
// 在小部件中使用
class SomeWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(myProvider);
    return Text(value);
  }
}
  • 代码示例:使用 GetX 状态管理

GetX 是一个全面的 Flutter 框架,它提供了状态管理、依赖注入和路由功能。

import 'package:get/get.dart';
 
class MyController extends GetxController {
  var someValue = "Initial Value".obs; // 创建一个可观察变量
}
 
// 在 MaterialApp 外部初始化控制器(可选,但推荐)
void main() {
  Get.put(MyController());
  runApp(MyApp());
}
 
// 在小部件中使用
class SomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.find<MyController>();
    return Obx(() => Text(controller.someValue.value)); // 使用 Obx 来监听变化
  }
}

7.4. 全局变量

  • 实现方式:在Flutter应用程序的顶层定义全局变量,并在不同的屏幕中访问和修改这些变量。

  • 优点

    • 适用于需要在整个应用程序中共享的数据。
    • 简单易用,不需要额外的状态管理工具。
  • 缺点

    • 全局变量可能导致代码难以维护和理解。
    • 容易出现数据竞争和状态不一致的问题。
  • 代码示例: 使用单例模式

单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。你可以创建一个包含全局变量的类,并将其实现为单例。

class GlobalVariables {
  static let instance = GlobalVariables()
  private init() {} // 私有构造函数,防止外部实例化
 
  var someValue: String?
}
 
// 使用
GlobalVariables.instance.someValue = "Hello, World!"
let value = GlobalVariables.instance.someValue

注意:在 Dart 和 Flutter 中,通常使用静态变量和函数来模拟单例模式,因为 Dart 没有真正的单例构造函数概念。上面的代码是 Swift 的示例,用于说明单例的概念。在 Dart 中,你可以这样做:

class GlobalVariables {
  static String? someValue;
}
 
// 使用
GlobalVariables.someValue = "Hello, World!";
let value = GlobalVariables.someValue;

7.5. 数据库

  • 实现方式:使用Flutter的数据库插件(如sqflite、moor等)在屏幕之间传递和存储数据。

  • 优点

    • 适用于需要持久化存储和查询数据的场景。
    • 可以在不同页面之间共享数据,而无需担心数据丢失。
  • 缺点

    • 需要学习并掌握数据库的使用和查询语法。
    • 可能会增加项目的复杂性和性能开销。

7.6 小结

对于大多数Flutter应用程序来说,推荐使用路由参数传递状态管理工具进行页面之间的传值。这两种方法都具有较高的灵活性和可扩展性,可以满足大多数场景的需求。

  • 如果需要传递的数据类型简单且数量较少,可以选择路由参数传递
  • 如果需要跨页面共享和管理复杂的状态,或者需要实现数据的实时更新和同步,建议选择状态管理工具

八、Deep Links

Flutter Deep Links(深度链接)是一种特殊类型的链接,它允许用户直接打开应用程序中的特定页面或执行特定操作,而无需通过应用程序的主屏幕或导航菜单进行手动搜索。以下是关于Flutter Deep Links的详细介绍:

8.1、作用与优势

  1. 直接访问特定页面:用户点击深度链接后,可以直接跳转到应用程序内的指定页面,提高了用户体验和转化率。
  2. 跨平台支持:Flutter Deep Links支持在iOS、Android和Web浏览器上使用,实现跨平台的无缝链接。
  3. 参数传递:深度链接中可以包含参数,用于传递用户信息、页面状态等,便于应用程序进行个性化展示和处理。

8.2、组成部分

Flutter Deep Links通常由以下几个部分组成:

  1. Schema:指定要打开的应用程序或资源的类型和协议,如httpsmyapp等。
  2. Host:指定资源所在的位置或域名,在Flutter应用程序中通常用于标识应用程序本身。
  3. Path:指定应用程序内具体页面的路径或位置。
  4. Query Parameter(可选):用于传递额外的查询参数或排序条件。
  5. Port(可选):一般用于访问后端服务时指定端口号,但在深度链接中较少使用。
  6. Fragment(可选):也称为锚点,用于直接跳转到网页或应用程序内页面的某个部分。

8.3、自定义模式与安全性

  1. 自定义模式:开发者可以自定义Schema和Host等部分,创建符合自己应用程序需求的深度链接格式。但需要注意安全性问题,防止恶意用户利用自定义模式进行非法访问。
  2. HTTPS模式:使用HTTPS协议创建的深度链接相对更安全,因为需要后端验证和应用程序的配合才能识别和处理。

84、配置与实现

  1. Android配置

    • AndroidManifest.xml文件中为需要处理深度链接的Activity添加<intent-filter>标签。
    • 指定Schema、Host、Path等必要信息。
    • 可以使用pathpathPatternpathPrefix等属性来匹配不同的路径模式。
  2. iOS配置

    • Info.plist文件中添加CFBundleURLTypes数组,指定自定义的URL Scheme。
    • 对于Universal Links,需要在apple-app-site-association文件中声明应用程序支持的域名和路径。
    • 在iOS项目中配置相关的entitlements和provisioning profiles。
  3. Flutter实现

    • 使用Flutter提供的路由系统(如Navigator)来处理深度链接的跳转。
    • 可以使用第三方库(如uni_links)来更方便地获取和处理深度链接。
    • 在应用程序启动时检查是否有深度链接传入,并根据需要进行处理。

8.5、应用场景

  1. H5唤醒APP:通过短信、邮件或社交媒体等渠道分享深度链接,用户点击后可以直接打开应用程序并跳转到指定页面。
  2. 其他APP跳转:其他应用程序可以通过深度链接直接跳转到本应用程序的特定页面,实现跨应用程序的导航和交互。
  3. 个性化推荐:根据用户的兴趣和行为数据,生成个性化的深度链接并推送给用户,提高用户粘性和转化率。

综上所述,Flutter Deep Links是一种强大的功能,可以帮助开发者实现跨平台的无缝链接和个性化推荐。但在使用过程中需要注意安全性和配置的正确性,以确保用户能够顺利访问到指定的页面或功能。

九、pubspec 管理

1、包、资源管理示例

以下是一个简单的pubspec.yaml文件示例,展示了如何在Flutter项目中使用Pub进行包管理:

name: my_flutter_app
description: A new Flutter project.
version: 1.0.0+1
 
environment:
  sdk: ">=2.12.0 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
  
  # 依赖pub仓库版本
  cupertino_icons: ^1.0.0
  http: ^0.13.3
  
dev_dependencies:
  flutter_test:
    sdk: flutter
 
flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/fonts/
  fonts:
    - family: Montserrat
      fonts:
        - asset: assets/fonts/Montserrat-Regular.ttf
        - asset: assets/fonts/Montserrat-Bold.ttf
          weight: 700

在这个示例中,我们定义了一个名为my_flutter_app的Flutter项目,并添加了fluttercupertino_iconshttp三个普通依赖项,以及一个flutter_test开发依赖项。同时,我们还在flutter字段中指定了项目的资源文件(图像和字体)的配置。

2、Flutter包管理

Flutter使用一种高效的包管理系统来管理和依赖项目的外部库和插件。这个系统基于pubspec.yaml文件,该文件位于项目的根目录下,并包含了项目的各种依赖项和配置信息。

  • 依赖项声明:在pubspec.yaml文件中,开发者可以声明项目所需的依赖项,包括普通依赖(在运行时需要的库)和开发依赖(仅在开发过程中需要的库,如测试框架)。
  • 版本管理:每个依赖项都可以指定一个版本号,Flutter包管理系统会确保下载并安装与指定版本兼容的包。同时,它支持版本约束和范围,允许开发者指定接受哪些版本的更新。
  • 包获取与更新:通过运行flutter pub get命令,Flutter会下载并安装项目中声明的所有依赖项。当依赖项有新版本发布时,开发者可以使用flutter pub upgrade命令来更新依赖项。

Pub

Pub是Dart编程语言的包管理器,也是Flutter项目的核心依赖管理工具。它提供了一个集中的仓库,开发者可以在其中查找、下载和发布Dart包。

  • 包仓库:Pub维护了一个包含大量Dart包的仓库,这些包涵盖了从基础库到特定功能的插件。
  • 包发布与查找:开发者可以将自己开发的Dart包发布到Pub仓库中,供其他开发者使用。同时,他们也可以在Pub仓库中查找并使用其他开发者发布的包。
  • 版本控制:Pub支持对包进行版本控制,允许开发者发布新版本的包,并管理旧版本的兼容性。

关键词解析

  • Flutter:一种开源的UI软件开发工具包,用于在iOS、Android、Web、Windows、Mac以及Linux等平台上构建美观的原生用户界面。
  • 包管理:指对项目中使用的外部库和插件进行管理和依赖管理的过程,包括声明依赖项、下载和安装依赖项、更新依赖项等。
  • Pub:Dart语言的包管理器,为Flutter项目提供核心依赖管理工具,支持包的查找、下载、发布和版本控制等功能。
  • pubspec.yaml:Flutter项目的配置文件,用于声明项目的依赖项、资源、配置信息等。

依赖方式

dependencies:
  flutter:
    sdk: flutter
  
  # 1、依赖pub仓库版本
  cupertino_icons: ^1.0.0
  http: ^0.13.3
  
  # 2、依赖本地包
  pkg1:
      path: ../../code/pkg1

   # 3、依赖Git:你也可以依赖存储在Git仓库中的包
   pkg2:
       git:
          url: git://github.com/xxx/pkg1.git

   # 4、上面假定包位于Git存储库的根目录中。如果不是这种情况,
   # 可以使用path参数指定相对位置:
    pkg3:
        git:
          url: git://github.com/flutter/packages.git
          path: packages/package1

3、资源管理

Flutter APP安装包包含代码和assets(资源)两部分。Assets在构建时被打包进程序安装包中,供运行时访问,包括静态数据(如JSON文件)、配置文件、图标、图片(JPEG、WebP、GIF等)等。

指定Assets

Flutter使用pubspec.yaml文件管理应用程序资源。通过在flutter部分的assets字段下列出文件路径,可以指定要包含在应用程序中的资源。资源路径是相对于pubspec.yaml文件的。

assets:
    - assets/my_icon.png
    - assets/background.png

Asset变体

构建过程支持“asset变体”概念,允许在不同上下文中显示不同版本的资源。在指定asset路径时,构建过程会在相邻子目录中查找同名文件,并将它们一起包含在资源包中。主资源和变体资源都会被打包。

.../graphics/background.png

assets:
    - graphics/background.png

加载Assets

  • 加载文本资源:通过rootBundleDefaultAssetBundle加载字符串资源。
  • 加载图片资源:使用AssetImage类加载图片,或通过Image.asset()方法直接获取显示图片的widget。Flutter支持根据设备像素比选择最接近分辨率的图片。
# 多倍图资源
.../my_icon.png 
.../2.0x/my_icon.png 
.../3.0x/my_icon.png

AssetImage加载图片

Widget build(BuildContext context) {
  return new DecoratedBox(
    decoration: new BoxDecoration(
      image: new DecorationImage(
        image: new AssetImage('graphics/background.png'),
      ),
), );
}

可以使用 Image.asset() 直接得到一个显示图片的widget:

Widget build(BuildContext context) {
  return Image.asset('graphics/background.png');
}

依赖包中的资源

要加载依赖包中的资源,必须在AssetImageImage.asset()方法中提供package参数,指定资源所属的包名。

# 使用AssetImage
new AssetImage('icons/heart.png', package: 'my_icons')

# Image.asset
new Image.asset('icons/heart.png', package: 'my_icons')

包也可以选择在其 lib/ 文件夹中包含未在其 pubspec.yaml 文件中声明的资源。 在这种情况下,对于 要打包的图片,应用程序必须在 pubspec.yaml 中指定包含哪些图像。

assets:
    - packages/fancy_backgrounds/backgrounds/background1.png

特定平台Assets

Flutter应用中的资源仅在Flutter框架运行后可用。对于APP图标和启动图等特定平台资源,需要按照Android或iOS的原生方式设置。

  • Android:在android/app/src/main/res目录中替换占位符图像,并更新AndroidManifest.xml中的android:icon属性。
  • iOS:在ios/Runner目录中的Assets.xcassets/AppIcon.appiconset替换占位符图片,保留原始文件名。

更新启动页

Flutter使用本地平台机制绘制启动页,直到Flutter渲染应用程序的第一帧。可以通过自定义drawable(Android)或storyboard(iOS)来添加自定义启动界面。

十、Flutter异常捕获

Flutter异常捕获必须先了解一下Dart单线程模型

image.png

1. Dart单线程模型

  • Java和Objective-C的对比

    • 在Java和Objective-C中,如果程序发生异常且未被捕获,程序将会终止。
    • 这是因为Java和Objective-C是多线程模型,任意一个线程触发未捕获的异常会导致整个进程退出。
  • Dart和JavaScript的对比

    • Dart和JavaScript是单线程模型,程序不会因为一个未捕获的异常而终止。
    • Dart通过消息循环机制运行,包含“微任务队列”(microtask queue)和“事件队列”(event queue)。
  • Dart线程运行过程

    • 入口函数main()执行完后,消息循环机制启动。
    • 微任务队列的优先级高于事件队列,按照先进先出的顺序执行。
    • 在事件任务执行过程中可以插入新的微任务和事件任务,形成循环,不会退出。
  • 任务类型

    • 外部事件任务(如IO、计时器、点击、绘制事件)在事件队列中。
    • 微任务通常来源于Dart内部,通过Future.microtask(...)方法插入。
  • 异常处理

    • 当某个任务发生异常且未被捕获时,程序不会退出,但当前任务的后续代码不会执行。
    • 一个任务中的异常不会影响其他任务执行。

2. Flutter异常捕获

  • Dart中的异常捕获

    • 使用try/catch/finally来捕获代码块异常,与其他编程语言类似。
  • Flutter框架异常捕获

    • Flutter框架在很多关键方法上进行了异常捕获。
    • 例如,当布局发生越界或不合规范时,Flutter会自动弹出一个错误界面。
    • 这是因为Flutter在执行build方法时添加了异常捕获。
@override
void performRebuild() {
 ...
    try { 
        //执行build方法 
        built = build();
    } catch (e, stack) {
        // 有异常时则弹出错误提示
        built = ErrorWidget.builder(_debugReportException('building $this', e,
    stack)); 
    }
    ... 
}

异常捕获和上报代码大致如下:

void collectLog(String line){ ... //收集日志

}

void reportErrorAndLog(FlutterErrorDetails details){
    ... //上报错误和日志逻辑 
}

FlutterErrorDetails makeDetails(Object obj, StackTrace stack){ 
    ...// 构建错误信息
}

void main() {
  // 在发生异常时,Flutter默认的处理方式是弹一个ErrorWidget
  // 进入 _debugReportException() 方法发现,错误是通过 FlutterError.reportError 方法中上报的
  
  // 自定义错误处理回调
  FlutterError.onError = (FlutterErrorDetails details) {
    reportErrorAndLog(details);
  };
  
  // Dart中有一个runZoned(...) 方法,可以给执行对象指定一个Zone。
  // Zone表示一个代码执行的环境 范围,为了方便理解,
  // 可以理解为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以 捕获、拦截或修改一些代码行为,
  // 如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时 Zone也可以捕获所有未处理的异常。
  runZoned(
    () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
        print: (Zone self, ZoneDelegate parent, Zone zone, String line) { 
        collectLog(line); // 收集日志
        }, 
    ),
    onError: (Object obj, StackTrace stack) {
      var details = makeDetails(obj, stack);
      reportErrorAndLog(details);
    }, 
   );
}