1.路由与导航

在lib下,新建pages目录,用来存放页面

  • 首页-Home.dart
  • 学习-Study.dart
  • 我-Mine.dart ```dart import ‘package:flutter/material.dart’; import ‘home/Home.dart’; import ‘study/Study.dart’; import ‘mine/Mine.dart’;

class Index extends StatefulWidget { Index({Key key}) : super(key: key);

@override _IndexState createState() => _IndexState(); }

class _IndexState extends State {

final List bottomNavItems = [ BottomNavigationBarItem( backgroundColor: Colors.blue, icon: Icon(Icons.home), label: ‘首页’, ), BottomNavigationBarItem( backgroundColor: Colors.green, icon: Icon(Icons.message), label: ‘学习’, ), BottomNavigationBarItem( backgroundColor: Colors.red, icon: Icon(Icons.person), label: ‘我’, ), ];

final List pages = [ { “appBar”: AppBar( title: Text(“拉勾教育”), centerTitle: true, elevation: 0, ), “page”: Home(), }, { “appBar”: AppBar( title: Text(“学习中心”), centerTitle: true, elevation: 0, ), “page”: Study(), }, { “appBar”: AppBar( title: Text(“个人中心”), centerTitle: true, elevation: 0, ), “page”: Mine(), }, ];

int currentIndex = 0;

@override Widget build(BuildContext context) { return Scaffold( appBar: pages[currentIndex][‘appBar’], bottomNavigationBar: BottomNavigationBar( items: bottomNavItems, currentIndex: currentIndex, selectedItemColor: Colors.green, type: BottomNavigationBarType.fixed, onTap: (index) { _changePage(index); } }, ), body: pages[currentIndex][‘page’], ); }

void _changePage(int index) { if (index != currentIndex) { setState(() { currentIndex = index; }); } }

@override void dispose() { super.dispose(); } }

  1. **Fluro路由**<br />首页完成后,我们需要点击课程列表,跳转到课程详情页。并且,需要动态传递参数,来动态获取详情页的内容。此时,我们需要声明详情页的路由。之前我们学过 Flutter 内置的路由方案(Navigator)。 <br />这里我们介绍一款企业级的路由框架 - Fluro
  2. - 安装
  3. ```yaml
  4. dependencies:
  5. fluro: ^1.7.8
  • 声明路由处理器

我们将所有路由文件,统一放到lib/routes中

lib routes RoutesHandler.dart 路由处理器 Routes.dart 路由

创建lib/routes/RoutesHandler.dart

  1. import 'package:flutter/material.dart';
  2. import 'package:fluro/fluro.dart';
  3. import '../pages/Index.dart';
  4. import '../pages/notfound/NotFound.dart';
  5. import '../pages/mine/ProviderTest.dart';
  6. import '../pages/course/CourseDetail.dart';
  7. import '../pages/mine/Profile.dart';
  8. /// 空页面
  9. var notfoundHandler = Handler(
  10. handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  11. return NotFound();
  12. }
  13. );
  14. /// 首页
  15. var indexHandler = Handler(
  16. handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  17. return Index();
  18. }
  19. );
  20. // 个人中心页面
  21. var mineHandler = new Handler(
  22. handlerFunc: (BuildContext context, Map<String, List<Object>> params) {
  23. return Mine();
  24. }
  25. );
  26. // 学习页面
  27. var studyHandler = new Handler(
  28. handlerFunc: (BuildContext context, Map<String, List<Object>> params) {
  29. return Study();
  30. }
  31. );
  • 声明路由

创建lib/routes/Routes.dart

  1. import 'package:flutter/material.dart';
  2. import 'package:fluro/fluro.dart';
  3. import 'RouteHandler.dart';
  4. class Routes {
  5. static void configureRoutes(FluroRouter router) {
  6. router.define('/', handler: indexHandler);
  7. router.define('/course_detail', handler: courseDetailHandler);
  8. router.define('/mine', handler: mineHandler);
  9. router.define('/study', handler: studyHandler);
  10. router.notFoundHandler = unknownHandler; // 未知页面
  11. }
  12. }

然后把路由相关的内容,也放到 lib/utils/Global.dart 中

  1. import 'package:fluro/fluro.dart';
  2. class G {
  3. /// Fluro路由
  4. static FluroRouter router;
  5. }

在入口文件(lib/main.dart)中初始化 router

  1. import 'package:fluro/fluro.dart';
  2. import 'routes/Routes.dart';
  3. import 'utils/global.dart';
  4. //final MyRouter router = MyRouter();
  5. void main() {
  6. FluroRouter router = FluroRouter();
  7. Routes.configureRoutes(router);
  8. G.router = router;
  9. // 初始化全局中的 router
  10. /// ...
  11. }
  • 使用路由

首页跳转到详情页

  1. /// course 是文章详情
  2. Map<String, dynamic> p = {
  3. 'id': course['id'],
  4. 'title': course['courseName'],
  5. };
  6. // print("/course_detail?id=123&title=课程名称");
  7. G.router.navigateTo(context, "/course_detail"+G.parseQuery(params: p));

上述代码中的parseQuery,是将Map类型,转成URL中query字符串,代码详情:

  1. // lib/utils/Global.dart
  2. import 'package:flutter/material.dart';
  3. class G {
  4. /// 将请求参数,由 Map 解析成 query
  5. static parseQuery({Map<String, dynamic> params}) {
  6. String query = "";
  7. if (params != null) {
  8. int index = 0;
  9. for (String key in params.keys) {
  10. final String value = Uri.encodeComponent(params[key].toString());
  11. if (index == 0) {
  12. query = "?";
  13. } else {
  14. query = query + "\&";
  15. }
  16. query += "$key=$value";
  17. index++;
  18. }
  19. }
  20. return query.toString();
  21. }
  22. }

2.状态管理

Provider

  • 安装Provider

https://pub.dev/packages/provider

  • 创建数据模型 ```dart import ‘package:flutter/material.dart’;

class currentIndexProvider with ChangeNotifier { int currentIndex = 0;

changeIndex(int index) { currentIndex = index; notifyListeners(); } }

  1. - 注册数据模型
  2. - 注册单个数据模型
  3. ```dart
  4. ChangeNotifierProvider(
  5. create: (BuildContext context) => new UserProvider(),
  6. child: MyApp()
  7. )
  • 注册多个数据模型
    1. MultiProvider(
    2. providers: [
    3. changNotifierProvider.value(value: CurrentIndexProvider()),
    4. changNotifierProvider.value(value: UserProvider()),
    5. ],
    6. child: MyApp()
    7. )
  • 在具体组件中使用Provider中的数据

访问Provider时,有两种方式i:监听和取消监听

  • 监听

    • 监听方法只能用来[StatelessWidget.build]和[State.build]中使用,监听值发生变化时,会重建组件
      1. Provider.of<T>(context)
      2. // 语法糖
      3. context.watch<T>(context)
  • 取消监听

    • 取消监听,不能在[StatelessWidget.build]和[State.build]中使用,换句话说,它可以在上述两个方法之外的所有方法中使用。监听值发生变化时,不会重建组件。
      1. Provider.of<T>(context, listen: false)
      2. // 语法糖 context
      3. context.read<T>
      访问数据
      1. Provider.of<CurrentIndexProvider>(context).currentIndex;
      访问方法
      1. // 取消监听
      2. Provider.of<CurrentIndexProvider>(context, listen: false).changeIndex(index);

      3.数据接口

      Dio
      安装Dio: https://pub.dev/packages/dio
      如果是使用http协议的接口地址,那么会产生报错,因为平台不支持不安全的HTTP协议,即不允许访问HTTP协议的地址。
  • Andriod解决
    • 打开andriod/app/src/main/AndriodManifest.xml ```dart

  1. - IOS解决
  2. - 打开ios/Runner/Info.plist。添加如下代码
  3. ```dart
  4. <key>NSAppTransportSecurity</key>
  5. <dict>
  6. <key>NSAllowsArbitraryLoads</key>
  7. <true/>
  8. </dict>

Dio 手册: https://github.com/flutterchina/dio/blob/master/README-ZH.md
初始化Dio
我们把所有接口操作相关的代码都集中放到api目录下。例如

lib api initDio.dart 初始化Dio AdAPI.dart 广告API UserAPI.dart 用户API API.dart 所有API

接下来,我们来创建具体的文件。首先,创建lib/api/initDio.dart

  1. Dio initDio() {
  2. BaseOptions _baseOptions = BaseOptions(
  3. baseUrl: 'http://xxx.com'
  4. )
  5. // 初始化
  6. Dio dio = Dio(_baseOptions);
  7. // 添加请求拦截
  8. dio.interceptors.add(
  9. Interceptorswrapper(
  10. // 请求拦截
  11. onRequest: (RequestOptions options) async {
  12. // 在请求被发送之前做的一些事情
  13. // 将access_token封装到header中,注意这里时dio文件因此不存在构建上下文,
  14. // 因此我们需要去获取,这里我们是在全局变量中定义了getCurrentContext()方法
  15. var user = G.getCurrentContext().read<UserProvider>().user;
  16. if (user.isNotEmpty) {
  17. optiosn.headers['Authorizatoin'] = user['access_token'];
  18. }
  19. return options;
  20. },
  21. // 响应拦截
  22. onResponse: (Resoponse response) async {
  23. // 在返回响应数据之前做一些预处理
  24. if (response.data['state'] != 1) {
  25. print('响应失败' + response.data['message']);
  26. response.data = null;
  27. }
  28. return response;
  29. },
  30. // 请求错误处理
  31. onError: (DioError e) async {
  32. return e;
  33. }
  34. )
  35. )
  36. }

如何在Flutter的任意位置获取构建上下文? 我们可以声明路由的全局唯一键(navigatorKey),然后,通过navigatorKey来获取构建上下文 在lib/utils/Global.dart中声明全局key

  1. class G {
  2. // 导入唯一key
  3. static final Globalkey<NavigatorState> navigatorKey = Globalkey();
  4. // 获取构建上下文
  5. static BuildContext getCurrentContext() =>
  6. navigatorkey.curentContext;
  7. }

在materialApp中注册navigatorKey

  1. MaterialApp(
  2. navigatorKey: G.navigatorKey
  3. )

然后调用G.getCurrentContext()就可以取到上下文

使用Dio
获取首页广告列表,创建lib/api/AdAPI.dart

  1. import 'package:dio/dio.dart';
  2. class AdAPI {
  3. final Dio _dio;
  4. AdAPI(THIS._dio);
  5. // 广告列表
  6. Future<dynamic> adList({
  7. String xxx = '999'
  8. }) async {
  9. Response res = await _dio.get('/xxx/xxx',
  10. queryParameters: {
  11. "xxx": xxx
  12. }
  13. );
  14. List adList = res.data;
  15. return adList;
  16. }
  17. }

创建lib/api/API.dart

  1. import 'package:dio/dio.dart';
  2. import 'initDio.dart';
  3. import 'AdAPI.dart';
  4. class API {
  5. Dio _dio;
  6. API() {
  7. // 初始化dio
  8. _dio = initDio();
  9. }
  10. // 广告接口
  11. AdAPI get ad => AdAPI(_dio);
  12. // 课程接口
  13. // CourseAPI get course => CourseAPI(_dio);
  14. }

为了操作方便,我们可以把常用内容统一放到一个全局文件中
列如,创建lib/utils/Global.dart。然后,把我们写好的接口放到Global.dart中

  1. import 'package:flutter/material.dart';
  2. import '../api/API.dart';
  3. class G {
  4. // 初始化API
  5. static final API api = API();
  6. }

在首页中调用接口adList()

  1. import '../api/API.dart';
  2. List adList = [];
  3. @override
  4. void initState() {
  5. super.initState();
  6. // 广告列表
  7. G.api.ad.adList().then(value) {
  8. setState() {
  9. adList = value.where((ad) => ad['status'] == 1).toList();
  10. }
  11. }
  12. }

首页
首页包含两部分内容:

  • 广告轮播

数据接口已经完成。想要展示轮播的话需要使用flutter_swiper插件。

  • 课程列表

创建lib/CourseAPI.dart。创建方式与AdAPI.dart一致,下面给出关键代码

  1. // 课程列表
  2. Future<dynamic> courseList() async {
  3. Response res = await _dio.get('/front/course/getAllCourse');
  4. List target = res.data['content'];
  5. return target;
  6. }

如果是初次创建lib/api/CourseAPI.dart,需要在lib/api/API.dart中,添加对应的gettter。

  1. import 'package:dio/dio.dart';
  2. import 'initDio.dart';
  3. class API {
  4. Dio _dio;
  5. API() {
  6. _dio = initDio();
  7. }
  8. CourseAPI get course => courseAPI(_dio);
  9. }

调用接口

  1. List courseList = [];
  2. @override
  3. void initState() {
  4. super.initState();
  5. // 课程列表
  6. G.api.course.courseList().then(value) {
  7. setState(() {
  8. courseList = value;
  9. })
  10. }
  11. }

展示数据

  1. // 展示数据的组件有多种,列如:ListView, GridView。我们这里使用SliverList:
  2. SliverList(
  3. delegate: SliverChildBuilderDelegate(BuildContext context, int index) {
  4. var course = courseList[index];
  5. // 创建列表想
  6. return GestureDetector(
  7. onTap: () {},
  8. child: 具体组件
  9. )
  10. }
  11. );

4.屏幕适配

除了内容展示之外还有屏幕适配的问题。例如,我的应用可能在手机打开,也可能在pad上打开。终端屏幕尺寸大小不易,如何进行屏幕适配呢?
适配原理

  • 设计尺寸(初始化指定,一般是设计师出图的尺寸,是可以预先知道的)
    • designWidth: 750px
    • designHeight: 1334px
  • 终端尺寸(动态获取)
    • deviceWidth: 1080px
    • deviceHeight: 1920px
  • 缩放比例
    • scaleWidth = deviceWidth / designWidth
    • scalHeight = deviceHeight / designHeight

明确了缩放比例后,我们可以适配终端了。
例如终端宽度是1080px,此时,如果声明50%的宽度,可以写成375.w(因为设计尺寸的宽度是750px,所以50%的宽度是375),375.w会根据缩放比例,计算处实际终端的宽度。计算公式为
实际宽度 50% = 375 _scaleWidth = 375 _(1080 / 750) = 540
flutter_screenutil
flutter_screenutil是用来解决屏幕适配的包。

详情查看:https://pub.dev/packages/flutter_screenutil

flutter_screenutil的工作原理是:在具体设备上,把原型图的尺寸,等比例放大或缩小
具体用法:
初始化设计尺寸

  1. ScreenUtilInit(
  2. designSize: Size(750, 1334), // 初始化设计尺寸
  3. allowFontScaling: false, // 字体大学奥是否跟随终端(这个有查阅文档,最新版本好像把这个属性删除了)
  4. builder: () => MaterialApp(
  5. title: 'Flutter_demo',
  6. onGenerateRoute: G.router.generator,
  7. initialRoute: '/'
  8. )
  9. );

在实际使用过程中,以Flutter1.2为分割线,有两种不同的写法

  • Flutter1.2之前

    1. width: ScreenUtil().setWidth(50);
    2. height: ScreenUtil().setHeight(200);
  • Flutter1.2之后

    1. width: 50.w;
    2. height: 200.h;

    5.富文本展示

    课程详情是一些HTML代码,但HTML不能直接在Flutter中展示。因此我们需要将HTML代码,转换成Flutter支持的Dart代码。这里我们借助flutter_html来完成课程详情的展示
    flutter_html

    详情查看:https://pub.dev/packages/flutter_html

使用步骤:

  • 安装

在pubspec.yaml中设置依赖

  1. dependencies:
  2. flutter_html: ^1.3.0

安装依赖
VS Code中,保存pubspec.yaml会自动安装图依赖
或者:
在Flutter项目根目录下运行

  1. flutter pub get
  • 配置gradle-plugin的中文镜像

为了能够通过Flutter的项目构建,我们需要对两个文件进行配置(目的是修改gradle-plugin的中文镜像)

  • 一个是Flutter安装路径下的文件。列如,我,本地把Flutter安装到D:\flutter,我的文件路径是D:\flutter\packages\flutter_tools\gradle\flutter.gradle

    1. buildscript {
    2. repositories {
    3. // google()
    4. // jcenter()
    5. maven { url 'https://maven.aliyun.com/repository/google' }
    6. maven { url 'https://maven.aliyun.com/repository/jcenter' }
    7. maven { url 'https://maven.aliyun.com/repository/public' }
    8. maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
    9. // 新加 }dependencies
    10. { classpath 'com.android.tools.build:gradle:4.1.0' }
    11. }
  • 修改flutter项目/android/build.gradle ```dart buildscript { ext.kotlin_version = ‘1.3.50’ repositories { // google() // jcenter() maven { url ‘https://maven.aliyun.com/repository/google‘ } maven { url ‘https://maven.aliyun.com/repository/jcenter‘ } maven { url ‘https://maven.aliyun.com/repository/public‘ } maven { url ‘https://maven.aliyun.com/repository/gradle-plugin/‘ } // 新加 } dependencies { classpath ‘com.android.tools.build:gradle:4.1.0’ classpath “org.jetbrains.kotlin:kotlin-gradle- plugin:$kotlin_version” } }

allprojects { repositories { // google() // jcenter() maven { url ‘https://maven.aliyun.com/repository/google‘ } maven { url ‘https://maven.aliyun.com/repository/jcenter‘ } maven { url ‘https://maven.aliyun.com/repository/public‘ } maven { url ‘https://maven.aliyun.com/repository/gradle-plugin/‘ } // 新加 } }

  1. - 使用
  2. ```dart
  3. import 'package:flutter_html/flutter_html.dart';
  4. Html(data: "<h1>标题</h1>"); // 在flutter中展示HTML代码

6.用户提示

toast常用于展示用户一些不那么重要的信息,会弹出并显示文字一段时间,时间一到就会消失,相较于snackbar和dialog,对屏幕的入侵较少。Flutter中最常用的toast组件时fluttertoast
fluttertoast的用法:

  • 安装

https://pub.dev/packages/fluttertoast

  • 引入

    1. import 'package:fluttertoast/fluttertoast.dart';
  • 使用

    1. ElevatedButton(
    2. child: Text('弹出toast'),
    3. onPressed: () {
    4. Fluttertoast: showToast(
    5. msg: '弹出消息内容', // 弹出信息
    6. toastLength: Toast.LENGTH_SHORT,
    7. gravity: ToastGravity.BOTTOM, // 展示位置
    8. timeInSecForIosweb: 1, // 秒数
    9. backgroundColor: Colors.black45, // 背景色
    10. fontSize: 16.0 // 字体大小
    11. )
    12. }
    13. )

    7.调用系统摄像头或打开相册

    调用终端的摄像头或者在相册中选取图片,我们需要使用第三方插件image_picker

    详情查看:https://pub.dev/packages/image_picker

  • 安装

在pubspec.yaml中,配置合适的版本

  • 配置权限

编辑andriod/app/src/main/AndriodManifest.xml

  1. <!-- 调用摄像头权限 -->
  2. <uses-permission android:name="android.permission.CAMERA" />
  3. <!-- 获取 SD 卡内容(访问相册)权限 -->
  4. <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
  5. <application android:requestLegacyExternalStorage="true"
  6. <!-- Android API 29+ 添加这一行 -->
  7. 其他配置项
  8. >
  • 声明调用函数 ```dart import ‘package:image_picker/image_picker.dart’; import ‘dart:io’;

final picker = ImagePicker(); File _image;

// 拍照获取图片 Future _takePhoto() async { final pickedFile = await picker.getImage(source: ImageSource: camera);

setState() { if (pickedFile != null) { _image = File(pickedFile.path); } else { print(‘No image’); } } }

// 在相册中选取一张图片 Future _openGallery() async { final pickedFile = await picker.getImage(source: ImageSource.gallery);

setState(() { if (pickedFile != null) { _image = File(pickedFile.path); } else { print(‘No Image’); } }); }

  1. - 声明调用菜单
  2. ```dart
  3. // 当触碰的时候拉起菜单,注册点击事件
  4. onTap: () {
  5. showModalBottomSheet(
  6. context: context,
  7. builder: (BuildContext context) {
  8. return renderBottomSheet(context);
  9. }
  10. )
  11. }
  12. Widget renderBottomSheet(BuildContext context) {
  13. return Container(
  14. height: 160,
  15. child: Column(
  16. children: [
  17. Inkwell(
  18. onTap: () {
  19. _takePhoto();
  20. G.router.pop(context);
  21. },
  22. child: Container(
  23. child: Text('拍照'),
  24. height: 50,
  25. alignment: Alignment.center
  26. )
  27. ),
  28. Inkwell(
  29. onTap: () {
  30. _takeGallery();
  31. G.router.pop(context);
  32. },
  33. child: Container(
  34. child: Text('从相册中选取'),
  35. height: 50,
  36. alignment: Alignment.center
  37. )
  38. ),
  39. Contaienr(
  40. color: Colors.grey[200],
  41. height: 10
  42. ),
  43. Inkwell(
  44. onTap: () {
  45. G.router.pop(context);
  46. },
  47. child: Container(
  48. child: Text('取消'),
  49. height: 50,
  50. alignment: Alignment.center
  51. )
  52. )
  53. ]
  54. )
  55. );
  56. }

8.支付

创建订单

  1. // 创建订单接口
  2. Future<dynamic> createOrder({ int goodsId }) async {
  3. Response res = await _dio.post('/front/order/saveOrder', data: {"goodsId": goodsId});
  4. return res.data['content'];
  5. }

发起支付

  1. // 发起支付接口
  2. Future<dynamic> createPay({
  3. String orderNo,
  4. int channel,
  5. String returnUrl = 'http://edufront.lagou.com'
  6. }) async {
  7. Map payData = {
  8. "goodsOrderNo": orderNo,
  9. "channel": channel == 1 ? 'weChat' : 'aliPay',
  10. "returnUrl": returnUrl
  11. }
  12. Response res = await _dio.post('/front/pay/saveOrder', data: payData);
  13. if (res.data != null) {
  14. return res.data['content'];
  15. } else {
  16. return false;
  17. }
  18. }

如果发起支付成功,上述接口会返回支付链接。接着,只要跳转到支付链接进行支付就可以了。
在APP中,跳转到指定URL地址,需要使用第三方插件url_launcher。

详情查看:https://pub.dev/packages/url_launcher

  1. import 'package:url_launcher/url_launcher.dart';
  2. // 跳转到指定页面
  3. void _launchURL(_url) async =>
  4. await canLaunch(_url)
  5. ?
  6. await launch(_url)
  7. :
  8. throw '不能跳转到 $_url';
  9. // 确定支付
  10. doPay() {
  11. // 发起支付
  12. G.api.order.createPay(orderNo: orderNo, channel: payment).then(value) {
  13. if (value != false) {
  14. _launchURL(value['payUrl']); // 跳转到支付链接
  15. } else {
  16. print('支付失败');
  17. }
  18. }
  19. }

9.Splash页面

Splash页面就是打开APP时,看到的第一个广告页。主要技术点是倒计时,默认展示广告图片,倒计时时间到了之后,跳转到首页。

  1. import 'package:flutter/material.dart';
  2. import 'dart.async';
  3. import '../../utils/Global.dart';
  4. class Splash extends StatefulWidget {
  5. Splash({Key key}) : super(key: key);
  6. @override
  7. _SplashState createState() => _SplashState();
  8. }
  9. class _SplashState extends State<Splash> {
  10. Timer _timer;
  11. int counter = 3;
  12. // 倒计时
  13. countDown() async {
  14. var _duration = Duration(seconds: 1);
  15. Timer(_duration, () {
  16. // 等待1秒之后,倒计时
  17. _timer = Timer.periodic(_duration, timer) {
  18. counter--;
  19. if (counter == 0) {
  20. // 执行跳转
  21. goHome();
  22. } else {
  23. setState(() {});
  24. }
  25. }
  26. return _timer;
  27. });
  28. }
  29. void goHome() {
  30. _timer.cancel();
  31. G.router.navigateTo(context, '/');
  32. }
  33. @override
  34. void initState() {
  35. super.initState();
  36. countDown(); // 指定倒计时
  37. }
  38. @override
  39. void initState() {
  40. super.initState();
  41. countDown();
  42. // 指定倒计时
  43. }
  44. @override Widget build(BuildContext context) {
  45. return Stack( alignment: Alignment(1.0, -1.0),
  46. children: [
  47. ConstrainedBox( constraints: BoxConstraints.expand(),
  48. child: Image.asset( "lib/assets/images/splash.jpeg",
  49. fit: BoxFit.fill ) ),
  50. Container( color: Colors.grey,
  51. margin: EdgeInsets.fromLTRB(0, 50, 10, 0),
  52. padding: EdgeInsets.all(5),
  53. child: TextButton(
  54. onPressed: () {
  55. goHome();
  56. },
  57. child: Text( "$counter 跳过广告",
  58. style: TextStyle(
  59. color: Colors.white,
  60. fontSize: 14 ) ), ) ), ] ); }
  61. @override
  62. void dispose() {
  63. super.dispose();
  64. }
  65. }

10.项目优化

虽然我们已经完成了项目的基本功能,但仍有很多细节,需要优化。
异步UI更新
试想这样一种场景:异步请求接口,在数据还未请求回来的时候,UI就已经更新了。此时,UI会因为拿不到数据而报错。
而异步UI更新,就是为了解决这一问题的。其基本思路是:先等待数据请求,后刷新UI
FutureBuilder 是对 Future 的封装。我们先来看看它的构造方法

  1. FutureBuilder({
  2. Key key,
  3. Future<dynamic> future,
  4. dynamic initialData,
  5. widget Function(BuildContext, AsyncSnapshot<dynamic>) builder
  6. })
  • future 接收 Future 类型的值,实际上就是我们的异步函数,通常是接口请求函数
  • initialData 初始数据,在异步请求完成之前使用
  • builder:是一个回调函数,接收两个参数一个 AsyncWidgetBuilder 类型的值

    1. builder: (
    2. BuildContext context,
    3. AsyncSnapshot<dynamic> snapshot
    4. ) {
    5. /// ...
    6. }

    AsyncSnapshot (即 snapshot)中封装了三个内容:

  • connectionState(连接状态 - 一共有四个)

    • none :当前未连接到任何异步计算。
    • waiting : 连接成功等待交互
    • active :正在交互中,可以理解为正在返回数据
    • done :交互完成,可以理解为数据返回完成。通过 snapshot.data 获取数据
  • data(实际上就是 future 执行后返回的数据)
  • error(实际上就是 future 错误时返回的错误信息)

保持页面状态
默认情况,我们进行页面跳转时。都会重新刷新页面(包括请求后代数据接口)。但是,有些页面的数据不会频繁变化(或及时性要求不高),此时,我们可以将页面数据暂时保存起来,从能避免页面频繁的刷新。
保持页面状态相当于缓存数据,是一种常规的优化手段。具体实现方案有如下几种

  • IndexedStack

IndexedStack 的逻辑是,一次加载多有的 Tab 页面,但同时,只展示其中一个。

  1. body: IndexedStack(
  2. index: curIndex,
  3. // children: _listViews,
  4. children: pages.map<Widget>((e) => e['widget']).toList(),
  5. )
  • AutomaticKeepAliveClientMixin ```dart // home page class Home extends StatefulWidget { Home({Key key}) : super(key: key);

    @override _HomeState createState() => _HomeState(); }

// 1. 使用 AutomaticKeepAliveClientMixin class _HomeState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true;

// 2. 声明 wantKeepAlive // 避免 initState 重复调用 @override void initState() { super.initState(); print(‘333333’); } @override Widget build(BuildContext context) { super.build(context); // 3. 在构造方法中调用父类的 build 方法 } /// … }

  1. - Tab 中,只保持某些页面的状态(需要修改 Tab 实现)
  2. - 声明 PageController
  3. ```dart
  4. PageController _pageController;
  • 初始化 PageController

    1. @override
    2. void initState() {
    3. // 2. 初始化 PageController
    4. _pageController = PageController(
    5. initialPage: G.getCurrentContext().watch<CurrentIndexProvider> ().currentIndex
    6. );
    7. super.initState();
    8. }
  • 修改 Tab 的 body

    1. body: PageView(
    2. controller: _pageController,
    3. children: pages.map<Widget>((e) => e['page']).toList(),
    4. )
  • 跳转到指定页面

    1. onTap: (index) async {
    2. // 4. 跳转到指定页面
    3. setState(() {
    4. _pageController.jumpToPage(index);
    5. });
    6. },

    11.DevTools

    DevTools 是一套 Dart 和 Flutter 性能调试工具。
    在开始 DevTools 之前,我们先来介绍一下 Flutter 的运行模式。
    Flutter 有四种运行模式:Debug、Release、Profile和test,这四种模式在build的时候是完全独立的。

  • debug

Debug 模式可以在真机和模拟器上同时运行:会打开所有的断言,包括 debugging 信息。debug 模式适合调试代码,但是不适合做性能分析。

命令 flutter run 就是以这种模式运行的。

  • release

Release 模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和 debugging 信息。关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。

命令 flutter run —release 就是以这种模式运行的

  • profile

Profile 模式只能在真机上运行,不能在模拟器上运行:基本和 Release 模式一致,除了启用了服务扩展和tracing。

命令 flutter run —profile 就是以这种模式运行的

  • test

headless test 模式只能在桌面上运行:基本和 Debug 模式一致,除了是 headless 的而且你能在桌面运行。

命令 flutter test 就是以这种模式运行的

判断当前运行环境

  1. // 当 App 运行在 Release 环境时,inProduction 为 true
  2. // 当 App 运行在 Debug 和 Profile 环境时,inProduction 为 false
  3. const bool inProduction = const bool.fromEnvironment("dart.vm.product");

目前,DevTools支持的功能有如下一些:

  • 检查和分析应用程序的UI布局和状态。
  • 诊断应用的UI性能问题。
  • 检测和分析应用程序的CPU使用情况。
  • 分析应用程序的网络使用情况。
  • Flutter或Dart应用程序的源代码级调试。
  • 调试Flutter或Dart应用程序的内存使用情况和分析内存问题。
  • 查看运行的Flutter或Dart应用程序的一般日志和诊断信息。

安装DevTools

  • 编辑器中

在 Android Studio 或 VS Code 中,只要你安装了 Flutter 插件,则 DevTools 也已经默认安装了。

  • 命令行中

    • 如果在你的环境变量 PATH 中有 pub , 可以运行:

      1. pub global activate devtools
    • 如果环境变量 PATH 中有 flutter , 可以运行:

      1. flutter pub global activate devtools

      启动 DevTools

  • VS Code中

image.png

  • 命令行中

    • 如果在你的环境变量 PATH 中有 pub , 可以运行:

      1. pub global run devtools
    • 如果环境变量 PATH 中有 flutter , 可以运行:

      1. flutter pub global run devtools

      image.png
      启动应用

  • debug 模式启动

    1. flutter run
  • profile 模式启动

    1. flutter run --profile

    使用DevTools

  • Flutter Inspector

这是一款用于可视化和浏览 Flutter Widget 树的工具。

  • Performance

性能分析

  • CPU Profiler

CPU 分析器,可以通过此视图记录应用的运行会话,并查看 CPU 在哪些方法中耗费了大量时间,然后就可以决定应该在哪里进行优化。

  • Memory

查看应用在特定时刻的内存使用情况

  • Debugger

调试器

  • Network

网络请求调试,例如:接口调试,HTTP 分析等

  • Logging

查看日志,支持关键词搜索。日志内容包括:

  • Dart 运行时的垃圾回收事件。
  • Flutter 框架事件,比如创建帧的事件。
  • 应用的 stdout 和 stderr 输出。
  • 应用的自定义日志事件。
    • App Size

App 打包后,可以对 APP 的大小进行分析
image.png

以 profile 方式启动 flutter (flutter run —profile)报错: Flutter Profile mode is not supported by sdk gphone x86. 原因:模拟器是 x86 的,不支持 profile 方式运行,将模拟器换成 x64 的即可。

12.架构原理

系统架构
首先,我们先来了解一下移动端架构的演进。只有了解了之前的技术架构,才能体会Flutter 的优势。

  • Native APP
    • Android
    • iOS
  • Web APP
    • SPA
    • PWA
  • HyBird APP
    • WebView(Cordova | Ionic | 微信小程序)
    • JSBridge(React Native | Weex)
    • 自绘制(Flutter)

原生架构
image.png
webview架构
image.png

没有使用原生组件,在 WebView 中通过 H5 实现所需的界面效果,对前端友好,上手快,成本低。 硬件通信,通过 Bridge 来完成。

JSBridge架构
image.png
Flutter架构
image.png
通信机制
image.png

Flutter跨端的通信效率也是高出JSBridge许多许多。Flutter通过Channel进行通信。其中: 1.BasicMessageChannel,用于传递字符串和半结构化的信息,是全双工的,可以双向请求数据。 2.MethodChannel,用于传递方案调用,即Dart侧可以调用原生侧的方法并通过Result接口回调结果数据。 3.EventChannel,用户数据流的通信,即Dart侧监听原生侧的实时消息,一旦原生侧产生了数据,立即回调给Dart侧

为什么我们说Channel的性能高呢。我们来看一下MethodChannel调用时的调用栈
image.png
整个流程中都是机器码的传递,而JNI的通信又和JavaVM内部通信效率一样,整个流程通信的流程相当于原生端的内部通信。因此,其通信效率比JSBridge要高。

具体来说,Flutter的系统架构共分三层,如下图:
image.png
Flutter Framework(框架层)
这是一个纯 Dart 实现的 SDK。从上往下包括了两大风格组件库(Material 和 Cupertino)、基础组件库、图形绘制、手势识别、动画等。

  • Widget

在 Flutter 中,可以把一切都看作一个组件,组件式的构建 UI。

  • Rendering

Rendering 是渲染库,在 Flutter 中,界面的渲染主要包括三个阶段:

  • 布局(Layout)
  • 绘制(Painting)
  • 合成(Composite)
    • Animation(动画)

Animation 是一个动画相关类,可以通过这个类创建一些基础的动画。

  • Painting(绘制)

Painting 封装了来自 Engine 层的绘制接口

  • Gesture(手势)

处理手势动作和手势相关交互

  • Foundation(基础)

底层框架,定义底层工具类和方法,提供其他层使用
Flutter Engine(引擎层)
这是一个 C++ 实现的 SDK,其中包括了 Skia 引擎(Google开源图形库)、Dart 运行时、文字排版引擎等。在代码调用 dart:ui 库时,调用最终会走到 Engine 层,然后实现真正的绘制逻辑。

Skia 是谷歌出品的开源二维图形库,提供常用的 API,并且可以在多种软硬件平台上运行。谷歌Chrome 浏览器、Chorme OS、Android、火狐浏览器和操作系统,及其他许多产品都使用它作为图形引擎。 和其他跨平台方案不同。Flutter 没有使用原生的 UI 和绘制框架,以此来保证 Flutter 的高性能体验。

Embedder(嵌入层)
嵌入层是操作系统适配层,其中主要负责的工作有:surface 渲染设置,线程的管理,原生插件管理,事件循环的交互等。
嵌入层位于整个框架的最底层,说明 Flutter 的平台相关层非常低,大部分的渲染操作在 Flutter 本身内部完成,各个平台(Android,iOS等)只需要提供一个画布,这就让 Flutter 本身有了很好的跨端一致性。

渲染过程
首先,我们从官网拿到下面这张图。这张图从用户的角度,来解释 Flutter 的渲染过程
image.png
另外,从源码的角度,对上述过程进行解剖,会得到下图
image.png
三颗树
从创建到渲染的大体流程是:
当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,同时与 Widget Tree 相对应,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。
这里我们可以做一个类比:

  • Widget Tree 是设计原型(设计师)
  • Element Tree 是产品原型(产品经理)
  • RenderObject Tree 要实现产品(工程师)

Widget Tree
第一棵树,是 Widget Tree。程序员写的用来构建页面的组件树。
需要注意的是,Widget 是不可变的(immutable),当 Widget 发生变化时,Flutter 会重建 Widget来进行更新。
那为什么将 Widget Tree 设计为 immutable?Flutter 主张 simple is fast,不用关心数据与节点之间的关系。

相当于牺牲空间换时间,从而保证性能

Element Tree
Element 就是 Widget 在 UI 树具体位置的一个实例化对象。持久存在于Dart Runtime上下文之中。它承载了构建的上下文数据,是Widget Tree 和 RenderObject Tree 的桥梁。
之所以让它持久地存在于 Dart 上下文中,而不是像 Widget 那样重新构建,因为Element Tree 的重新创建和重新渲染的开销会非常大,所以 Element Tree 到 RenderObject Tree 也有一个 Diff 环节,来计算最小重绘区域。
需要注意的是,Element 同时持有 Widget 和 RenderObject,但无论是 Widget 还是 Element,其实都不负责最后的渲染,它们只是“发号施令”,真正对配置信息进行渲染的是 RenderObject。
RenderObject Tree
渲染树的任务就是做具体渲染工作。RenderObject 用于应用界面的布局(Layout)绘制 (Paint),保存了元素的大小,布局等信息。RenderObject 主要属性和方法如下:

  • constraints 对象,从其父级传递给它的约束
  • parentData 对象,其父对象附加有用的信息。
  • performLayout 方法,计算此渲染对象的布局。
  • paint 方法,绘制该组件及其子组件。

布局过程
Flutter 中的组件在屏幕上绘制渲染之前,需要先进行布局(Layout)操作。其具体可分为两个过程:

  • Constraints Down(从顶部向下传递约束)

父节点给每个子节点传递约束,这些约束是每个子节点在布局阶段必须要遵守的规则。

常见的约束包括,规定子节点最大最小宽度或者子节点最大最小的高度。这种约束会向下延伸,子组件也会产生约束传递给自己的孩子,一直到叶子结点。

  • Layout Up(从底部向上传递布局信息)

子节点接受到来自父节点的约束后,会依据这些约束,产生自己的布局信息;

例如:父节点规定我的最小宽度是 500 的单位像素,子节点按照这个规则,可能定义自己的宽度为 500 个像素,或者大于 500 像素的任何一个值。

  1. 确定好自己的布局信息之后,将这些信息告诉父节点。父节点也会继续此操作向上传递,一直到最顶部。

Flutter 中有两种主要的布局协议:Box 盒子协议和 Sliver 滑动协议。RenderObject 作为一个抽象类。每个节点需要实现它才能进行实际渲染。扩展 RenderOject 的两个最重要的类是 RenderBox 和RenderSliver。这两个类分别是应用了 Box 协议和 Sliver 协议。
绘制过程
RenderObject 可以通过 paint() 方法来完成具体绘制逻辑,流程和布局流程相似,子类可以实现 paint() 方法来完成自身的绘制逻辑。

  1. void paint(PaintingContext context, Offset offset) { }

通过 context.canvas 可以取到 Canvas 对象,接下来就可以调用 Canvas API 来实现具体的绘制逻辑。 如果节点有子节点,它除了完成自身绘制逻辑之外,还要通过 paintChild() 方法来调用子节点的绘制方法。 如此递归完成整个节点树的绘制,最终调用栈为: paint() > paintChild() > paint() … 。

另外,Flutter 使用 Composited Layer 来对 RenderObject 的绘制进行组织,通常一个 Composited Layer 对应一棵 RenderObject 子树,Composited Layer 的 Display List 记录了这棵 RenderObject 子树的绘制指令。
为什么需要三棵树?
使用三棵树的目的是尽可能复用 Element。复用 Element 对性能非常重要,因为 Element 拥有两份关键数据:StatefulWidget 的状态对象及底层的 RenderObject。当应用的结构很简单时,或许体现不出这种优势,一旦应用复杂起来,构成页面的元素越来越多,重新创建 3 棵树的代价是很高的,所以需要最小化更新操作。
Element Tree的定位,有点象Web端的Virtual DOM
image.png
在开始 Flutter 的渲染机制之前,我们先介绍一下屏幕绘制的原理
image.png
我们知道显示器以固定的频率刷新,比如 iPhone的 60Hz。当一帧图像绘制完毕后,准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync)。一般地来说,计算机中,CPU、GPU 和显示器以一种特定的方式协作:
CPU 将计算好的显示内容提交给 GPU,GPU 渲染后放入帧缓冲区,然后视频控制器按照 VSync 信号从帧缓冲区取帧数据,传递给显示器显示。屏幕上的每一帧的绘制过程,实际上是 Engine 通过接收的VSync 信号不断地触发帧的绘制。
绘制管线
image.png
Flutter 只关心向 GPU 提供视图数据,GPU 的 VSync信号同步到 UI 线程,UI 线程使用 Dart 来构建抽象的视图结构,这份视图结构在 GPU 线程进行图层合成,视图数据提供给 Skia 引擎渲染为 GPU 数据,这些数据通过 OpenGL 或 Vulkan提供给 GPU。
Flutter 的渲染流水线也包括两个线程 —— UI 线程 GPU 线程
UI 线程主要负责的是根据 UI 界面的描述生成 UI 界面的绘制指令,而 GPU 线程负责光栅化和合成。
渲染管线
在 Flutter 框架中存在着一个渲染流水线(Rendering pipline)。这个渲染流水线是由垂直同步信号 (Vsync)驱动的,而 Vsync 信号是由系统提供的,如果你的 Flutter 是运行在 Android 上的话,Flutter 会向 Android 系统的 Choreographer 注册并接收 VSync 信号,GPU 硬件产生 VSync 信号以后,系统便会触发回调,并驱动 UI 线程进行渲染工作。
image.png

  • 动画(Animate)阶段:因为动画会随每个Vsync信号的到来而改变状态(State),所以动画阶段是流水线的第一个阶段。
  • 构建(Build)需要被重新构建的 Widget 会在此时被重新构建。也就是我们熟悉的 StatelessWidget.build() 或者 State.build() 被调用的时候。
  • 布局(Layout)阶段,这时会确定各个显示元素的位置,尺寸。此时是RenderObject.performLayout() 被调用的时候。
  • 绘制(Paint)阶段,此时是 RenderObject.paint() 被调用的时候。

渲染流水线
image.png
启动过程分析
接下来,我们以 Flutter 的启动过程为例,来分析一下 Flutter 的源码。
image.png
上述图比较复杂,你可以先简单了解下,等下我们会详细拆分来讲解。我们先来看下这几个关键函数的作用。
首先我们从 runApp() 开始

  1. void main() { runApp(MyApp()); }

runApp() 函数声明在 widgets/binding.dart 中

  1. void runApp(Widget app) {
  2. WidgetsFlutterBinding.ensureInitialized() /// 单例模式初始化
  3. ..scheduleAttachRootWidget(app) /// 将 app 添加到根组件
  4. ..scheduleWarmUpFrame(); /// 调度热身帧
  5. }
  • ensureInitialized 实例化过程中,实现了很多绑定

    1. class WidgetsFlutterBinding extends BindingBase with
    2. GestureBinding, /// 手势绑定
    3. ServicesBinding, /// 服务绑定
    4. SchedulerBinding, /// 调度绑定
    5. PaintingBinding, /// 绘制绑定
    6. SemanticsBinding, /// 语义绑定(辅助功能)
    7. RendererBinding, /// 渲染绑定
    8. WidgetsBinding /// 组件绑定
    9. { static WidgetsBinding ensureInitialized() {
    10. /// 单例模式实例化
    11. if (WidgetsBinding.instance == null) WidgetsFlutterBinding()
    12. ; return WidgetsBinding.instance;
    13. }
    14. }
  • scheduleAttachRootWidget,创建根 widget ,并且从根 widget 向子节点递归创建元素Element,对子节点为 RenderObjectWidget 的小部件创建 RenderObject 树节点,从而创建出View 的渲染树,这里源代码中使用 Timer.run 事件任务的方式来运行,目的是避免影响到微任务的执行。

    1. void scheduleAttachRootWidget(Widget rootWidget) {
    2. Timer.run(() {
    3. attachRootWidget(rootWidget);
    4. });
    5. }
  • attachRootWidget 与 scheduleAttachRootWidget 作用一致,首先是创建根节点,然后调用attachToRenderTree 循环创建子节点。

    1. void attachRootWidget(Widget rootWidget) {
    2. _readyToProduceFrames = true;
    3. _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    4. container: renderView,
    5. debugShortDescription: '[root]',
    6. child: rootWidget,
    7. ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
    8. }
  • attachToRenderTree,该方法中有两个比较关键的调用,我只举例出核心代码部分,这里会先执行 buildScope ,但是在 buildScope 中会优先调用第二个参数(回调函数,也就是element.mount ),而 mount 就会循环创建子节点,并在创建的过程中将需要更新的数据标记为dirty。

    1. owner.buildScope(element, () {
    2. element.mount(null, null);
    3. });
  • buildScope,如果首次渲染 dirty 是空的列表,因此首次渲染在该函数中是没有任何执行流程的, 该函数的核心还是在第二次渲染或者 setState 后,有标记 dirty 的 Element 时才会起作用,该函数的目的也是循环 dirty 数组,如果 Element 有 child 则会递归判断子元素,并进行子元素的 build创建新的 Element 或者修改 Element 或者创建 RenderObject。

  • updateChild,该方法非常重要,所有子节点的处理都是经过该函数,在该函数中 Flutter 会处理Element 与 RenderObject 的转化逻辑,通过 Element 树的中间状态来减少对 RenderObject 树的影响,从而提升性能。具体这个函数的代码逻辑,我们拆解来分析。

该函数的输入参数,包括三个参数:

  • Element child

child 为当前节点的 Element 信息

  • Widget newWidget

newWidget 为 Widget 树的新节点

  • dynamic newSlot

newSlot 为节点的新位置
在了解参数后,接下来看下核心逻辑,首先判断是否有新的 Widget 节点。

  1. if (newWidget == null) {
  2. if (child != null)
  3. deactivateChild(child);
  4. return null;
  5. }

如果不存在,则将当前节点的 Element 直接销毁
如果 Widget 存在该节点,并且 Element 中也存在该节点,那么就首先判断两个节点是否一致,

  • 如果一致只是位置不同,则更新位置即可。
  • 其他情况下判断是否可更新子节点,如果可以则更新,如果不可以则销毁原来的 Element 子节点,并重新创建一个。

    1. if (hasSameSuperclass && child.widget == newWidget) {
    2. if (child.slot != newSlot)
    3. updateSlotForChild(child, newSlot);
    4. newChild = child; } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
    5. if (child.slot != newSlot)
    6. updateSlotForChild(child, newSlot);
    7. child.update(newWidget);
    8. // 根据不同的节点类型,调用不同的
    9. Update assert(child.widget == newWidget);
    10. assert(() {
    11. child.owner._debugElementWasRebuilt(child);
    12. return true;
    13. }());
    14. newChild = child;
    15. } else {
    16. deactivateChild(child);
    17. assert(child._parent == null);
    18. newChild = inflateWidget(newWidget, newSlot);
    19. }

    上面代码的第 8 行非常关键,在 child.update 函数逻辑里面,会根据当前节点的类型,调用不同 的 update ,可参考上图中的 update 下的流程,每一个流程也都会递归调用子节点,并循环返回 到 updateChild 中。有以下三个核心的函数会重新进入 updateChild 流程中,分别是 performRebuild、inflateWidget 和 markNeedsBuild,接下来我们看下这三个函数具体的作用。

  • performRebuild 是非常关键的一个代码,这部分就是我们在组件中写的 build 逻辑函数,StatelessWidget 和 StatefulWidget 的 build 函数都是在此执行,执行完成后将作为该节点的子节点,并进入 updateChild 递归函数中。

  • inflateWidget 创建一个新的节点,在创建完成后会根据当前 Element 类型,判断是

RenderObjectElement 或者 ComponentElement 。根据两者类型的不同,调用不mount, 挂载 到当前节点上,在两种类型的 mount 中又会循环子节点,调用 updateChild 重新进入子节点更新流程。这里还有一点,当为 RenderObjectElement 的时候会去创建 RenderObject 。

  • markNeedsBuild,标记为 dirty ,并且调用 scheduleBuildFor 等待下一次 buildScope 操作。

首次Build
当我们首次加载一个页面组件的时候,由于所有节点都是不存在的,因此这时候的流程大部分情况下都是创建新的节点,如下图:
image.png
image.png
runApp 到 RenderObjectToWidgetElement(mount) 逻辑都是一样的,在 _rebuild 中会调用updateChild 更新节点,由于节点是不存在的,因此这时候就调用 inflateWidget 来创建 Element。
Element 为 Component 时,会调用 Component.mount ,在 Component.mount 中会创建Element 并挂载到当前节点上,其次会调用 _firstBuild 进行子组件的 build ,build 完成后则将 build 好的组件作为子组件,进入 updateChild 的子组件更新。
Element 为 RenderObjectElement 时,则会调用 RenderObjectElement.mount,在
RenderObjectElement.mount 中会创建 RenderObjectElement 并且调用createRenderObject 创建RenderObject,并将该 RenderObject 和 RenderObjectElement 分别挂载到当前节点的 Element 树和 RenderObject 树,最后同样会调用 updateChild 来递归创建子节点。
以上就是首次 build 的逻辑,单独来看还是非常清晰的,接下来我们看下 setState 的逻辑
setState
首先,我们查看 StatefulWidget 中的 setState

  1. void setState(VoidCallback fn) {
  2. /// ...
  3. _element.markNeedsBuild();
  4. // 标记需要构建的 element
  5. }

然后,我们来看一下 markNeedsBuild

  1. void markNeedsBuild() {
  2. /// ...
  3. if (dirty) return;
  4. _dirty = true;
  5. owner.scheduleBuildFor(this);
  6. }

Widget 对应的 element 将自身标记为 dirty 状态,并调用 owner.scheduleBuildFor(this); 通知 buildOwner 进行处理。

  1. void scheduleBuildFor(Element element) {
  2. ...
  3. if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
  4. _scheduledFlushDirtyElements = true;
  5. onBuildScheduled();
  6. // 这是一个callback,调用的方法是下面的
  7. _handleBuildScheduled }
  8. _dirtyElements.add(element);
  9. // 把当前 element 添加到 _dirtyElements 数组里面,后 面重新build会遍历这个数组
  10. element._inDirtyList = true;
  11. }

此时 buildOwner 会将所有 dirty 的 Element 添加到 _dirtyElements 当中经过 Framework 一连串的调用后,最终调用 scheduleFrame 来通知 Engine 需要更新 UI,Engine 就会在下个 vSync 到达的时候通过调用 _drawFrame 来通知 Framework,然后 Framework 就会通过 BuildOwner 进行Build 和PipelineOwner 进行 Layout,Paint,最后把生成 Layer,组合成 Scene 提交给 Engine。

  1. void _drawFrame() {
  2. // Engine 回调 Framework 入口
  3. _invoke(window.onDrawFrame, window._onDrawFrameZone);
  4. }
  5. void initInstances() {
  6. super.initInstances();
  7. _instance = this;
  8. ui.window.onBeginFrame = _handleBeginFrame;
  9. // 初始化的时候把 onDrawFrame 设置为 _handleDrawFrame
  10. ui.window.onDrawFrame = _handleDrawFrame;
  11. SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  12. }
  13. void _handleDrawFrame() {
  14. if (_ignoreNextEngineDrawFrame) {
  15. _ignoreNextEngineDrawFrame = false;
  16. return;
  17. }
  18. handleDrawFrame();
  19. }
  20. void handleDrawFrame() {
  21. _schedulerPhase = SchedulerPhase.persistentCallbacks;
  22. // 记录当前更新UI的状态
  23. for (FrameCallback callback in _persistentCallbacks)
  24. _invokeFrameCallback(callback, _currentFrameTimeStamp);
  25. }
  26. }
  27. void initInstances() {
  28. /// ....
  29. addPersistentFrameCallback(_handlePersistentFrameCallback);
  30. }
  31. void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); }
  32. void drawFrame() {
  33. /// ...
  34. if (renderViewElement != null)
  35. buildOwner.buildScope(renderViewElement);
  36. // 先重新build widget
  37. super.drawFrame();
  38. buildOwner.finalizeTree();
  39. }

核心方法 buildScope

  1. void buildScope(Element context, [VoidCallback callback]){
  2. /// ...
  3. }

需要传入一个 Element 的参数,这个方法通过字面意思应该理解就是对这个 Element 以下范围 rebuild

  1. void buildScope(Element context, [VoidCallback callback]) {
  2. /// ...
  3. try {
  4. /// ...
  5. _dirtyElements.sort(Element._sort);
  6. // 1.排序 /// ...
  7. int dirtyCount = _dirtyElements.length;
  8. int index = 0;
  9. while (index < dirtyCount) {
  10. try { _dirtyElements[index].rebuild();
  11. // 2.遍历 rebuild
  12. } catch (e, stack) {} index += 1; }
  13. } finally {
  14. for (Element element in _dirtyElements) {
  15. element._inDirtyList = false;
  16. }
  17. _dirtyElements.clear();
  18. // 3.清空 /// ...
  19. }
  20. }
  • 第1步:按照 Element 的深度从小到大,对 _dirtyElements 进行排序

由于父 Widget 的 build 方法必然会触发子 Widget 的 build,如果先 build 了子 Widget,后面再build 父Widget 时,子 Widget 又要被 build 一次。所以这样排序之后,可以避免子 Widget 的重复 build。

  • 第2步:遍历执行 _dirtyElements 当中 element 的 rebuild 方法

值得一提的是,遍历执行的过程中,也有可能会有新的 element 被加入到 _dirtyElements 集合中,此时会根据 dirtyElements 集合的长度判断是否有新的元素进来了,如果有,就重新排序。

element 的 rebuild 方法最终会调用 performRebuild(),而 performRebuild() 不同的 Element 有不同的实现。

  • 第3步:遍历结束之后,清空 dirtyElements 集合

因此 setState() 的主要工作是记录所有的脏元素,添加到 BuildOwner 对象的 _dirtyElements 中,然后调用scheduleFrame 来注册 Vsync 回调。 当下一次 Vsync 信号到来时,会执行 handleBeginFrame()和handleDrawFrame() 来更新 UI。
image.png
image.png