初识Flutter

Posted by Chejdj Blog on July 30, 2021

 来到一个新公司,业务使用是Flutter进行开发,之前说实话我对Flutter是比较悲观的,感觉对于跨平台的技术,我个人认为发展到后期导致无人维护的状态而烂尾。花费了一周的时间,把《Flutter实战》敲了一边之后,纠正了以前的观点(公众号害人不浅),站在开发角度我认为具有一下几个有点:

  • Flutter只是一种跨端的UI实现方案,过于夸大了Flutter的作用,跨平台只是跨的UI平台,很多功能像调摄像头,启前台service等一切涉及到Framework的调用的地方都需要Android原生实现。
  • UI一致性,现在我们移动端与网页端其实使用的是同一套UI布局,但是因为各端实现方式不一样,需要写3套代码,经常写代码的都知道,同一份逻辑写多份,容易导致不一致问题
  • Flutter可以作为大前端项目的组织形式,可以做各个端相同特性统一管理(资源管理)
  • 项目组件化,可以说Flutter的组织形式就是以组件化构成的(后面提到)
    当然新的技术也会存在它的弊端(也不新,2018年12月Flutter 1.0版本发布):
  • 质量好的三方库较少,一方面是质量,一方面是功能,可能很多需要自己单独实现
  • 由于Flutter对于插件,library的粒度较小,库与库之间版本依赖较为复杂,有的时候不得不因为A库依赖B库的低版本,但是项目中已经在使用B库的高版本,A库作者没有及时适配,要不不用,要么自己适配
  • Dart语言的可读性较差

Flutter项目的组织形式

  1. Flutter Application: 纯正的Flutter项目,可以同时构建初Android,ios App
  2. Flutter plugin: 插件,非纯Dart代码项目,包含了一些内部Android,ios的本地实现,经常是一个功能模块的实现,对外只提供调用接口
  3. Flutter Package: 纯正的Dart语言项目,封装一些使用Dart实现的功能,wdiget等
  4. Flutter Module: 主要用于在原生的Android或者ios项目当中,想使用Flutter就可以使用这个,经常在想要在原生的项目中打算迁移到Flutter的中间过度的一种方式

Dart 语言

官方网站主要描述Dart语言特点:为UI构建优化, 研发生产力提高,在全平台极速运行.

一. 为UI构建优化

  1. 为基于事件驱动的用户界面提供成熟且完备的 异步等待 体系,同时配备 基于isolate的并发
    • 异步等待体系 : 不再是像Java那样线程池,它没有,简单的使用Future, async进行异步功能调用,await来控制异步函数的调用顺序
    • 基于isloate的并发 : 新概念 “隔离区”,大多数计算机中,甚至移动平台中,都使用多核cpu, 为了有效利用多核性能,开发者一般使用共享内存的方式让线程并发地运行,然而,多线程共享数据导致很多潜在问题,所以为了彻底解决多线程的并发问题,Dart使用了isolate替代线程,所有Dart代码均运行在一个isloate中,每个isolate有自己的堆内存确保状态不被其他isolate访问
  2. 基于isloate的并发 为了支持异步,提出了所谓“隔离区”的概念,Dart语言是单线程。 看一参考这篇文章的顺序: http://www.ruanyifeng.com/blog/2014/10/event-loop.html 简单来说最主要的区别可以看下面伪代码:
1
2
执行异步任务... //步骤一
print('123') //步骤二

在Java中步骤一和步骤二的顺序是不固定的,但是在Dart语言中,步骤二一定会在步骤一之前执行。因为Dart 单线程执行引入了一个所谓的 “Event loop” 任务队列的概念,当异步任务执行完之后会在“任务队列”之中放置一个事件, Dart执行完当前执行栈的所有代码之后,会不断读取 “Event loop”的代码,然后执行挂起的代码。

  1. 空安全(和Kotlin一样)以及一些集合流操作

  2. 重要的几个概念

    • Dart 所有变量的值都是对象
    • Dart强类型语言,但是显式变量类型声明可选,Dart支持类型推断,如果不想使用类型推断,使用dynamic
    • Dart支持顶层变量和类成员变量
    • 没有public, protected和private关键字,使用下划线“_”开头的变量或者函数
    • Dart内存分配,简单,创建对象的时候在现有堆上移动指针,内存增长线形的
    • Dart垃圾回收多生代算法,新生代采用“半空间”算法,触发回收,将活跃对象拷贝备用空间,然后整体释放当前空间内存

二. 研发生产力提高

热重载(快速看到结果),可配置静态分析工具,性能分析的调试工具

三. 在全平台极速运行

  1. 使用 AOT 编译为机器码,极速启动应用 移动和桌面设备应用程序:拥有实(JIT)编译功能的Dart VM 和用于生成机器代码的提前AOT编译器
  2. 使用完整,成熟且快速的JavaScripe编译器为Web编译应用,使用Dart2js编译城JavaScript代码
  3. 仅使用一种语言编写应用的后端代码

Dart语言的使用

可以参考官方文档,还是非常详细的 https://dart.cn/guides/language/effective-dart

Flutter UI基础框架

Flutter中的三颗树

  1. Widget树:描述UI元素的配置数据,就是UI配置树,真正的UI渲染树是由Element构成的
  2. Element树: Widget在UI树具体位置的一个实例化对象, 大多数Element只有唯一的 rendorObject,也有一些Element会有多个子节点
  3. RenderObject树: 组件的布局,渲染都是通过RenderObject进行完成的 Element的生命周期如下:
  4. Widget创建Element实例
  5. elemnet调用mount方法,将Widget创建的renderObject 添加到渲染树中指定位置,然后element处于active状态,就可以显示
  6. 当widget配置数据变化,重新构建对应Element树,为了进行Element复用,如果可以更新配置
  7. 当有祖先element决定移除element的时候,置为inactive

    Flutter的状态管理

    状态管理一下以下几种情况:

    • Widget管理自己的状态: 如果状态是有关界面外观效果的,例如颜色动画,则状态由本身管理
    • Widget管理子Widget的状态: 如果状态是用户数据,像复选框的选中状态、滑块位置,则状态最好由父类Widget管理
    • 混合管理(父Widget和子Widget都管理状态):这个情况不建议,因为如果某个状态是不同Widget共享则最好由它们共同的父Widget管理
    • 全局的状态管理:比如App内切换主题,切换语言等。一般两种方式: (1)实现一个全局的事件总线,app内依赖应用语言的组件订阅改变 (2)使用专门的状态管理包,如Provider, Redux

Flutter功能框架之’InheritedWidget’

InheritedWidget:提供数据在widget树中从上到下传递、共享的方式。
方式State中有一个didChangeDependencies回调,若子Widget使用了父Widget中InheritedWidget的数据,发生变化的时候会进行调用
需要关注的点:

  1. 如果子Widget的build中没有使用父类的InheritedWidget数据,不会产生回调 didChangeDependencies
  2. context.dependOnInheritedWidgetOfExactType<XXX>数据发生变化的时候注册回调子Widget的didChangeDependencies,但是如果使用context.getElementForInheritedWidgetOfExactType<XXX>().widget就没有注册回调,也不会发生变化

Flutter跨组件的状态管理’Provider’

Provider实现状态管理的原理就是InheritedWidget能与依赖它的子孙组件,当InheritedWidget数据发生变化的时候,可以自动更新依赖子孙组件的原理实现了一套跨组件状态共享解决方案。
《Flutter实战》这本书里面例子详细介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 通用的InheritedWidget 保存任何需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget{
   InheritedProvider({@required this.data, Widget child}): super(child: child);

   //共享状态使用范型
   final T data;


   //控制依赖该数据的子Widget是否需要重新rebuild
   @override
   bool updateShouldNotify(InheritedProvider<T> old){
      //在此简单返回true, 则每次更新都会调用依赖其的子孙节点的'didChangeDependencies'
      return true;
   }
}


class ChangeNotifier implements Listenable{
   List listeners=[];

   @override
   void addListener(VoidCallback listener){
      listeners.add(listener);
   }
   
   @override
   void removeListener(VoidCallback listener){
      listeners.remove(listener);
   }

   void notifyListeners(){
      listeners.forEach((item) => itme());
   }
}

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefuleWidget{
   ChangeNotifierProvider({
      Key key,
      this.data,
      this.child,
   });
   final Widget child;
   final T data;

   //定义便捷的方法,方便子树中的widget获取共享数据
   static T of<T>(BuildContext context){
      final type = _typeOf<InheritedProvider<T>>();
      final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
      return provider.data;
   }

   @override
   _ChangeNotifierProviderState<T> create() => _ChangeNOtifierProviderState<T>();
}

class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifier<T>>{
    void update(){
       //如果数据发生变化(model 类调用类notifyListeners),重新构建InheritedProvider
       setState(() => {});
    }


   //经常用于父Widget重新创建的时候或者Widget 配置改变的时候
    @override 
    void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
         //当Provider更新时,如果新旧数据不“==”,则绑定数据监听。同时添加新数据监听
        if(widget.data != oldWidget.data){
            oldWidget.data .removeListener(update);
            widget.data.addListener(update);
         }
         super.didUpdateWidget(oldWidget);
   }

   @override
   void initState(){
      // 给model添加监听器
      widget.data.addListener(update);
      super.initState();
   }

   @override
   void dispose(){
      //移除model的监听器
      widget.data.removeListener(update);
      super.dispose();
   }

   @override
   Widget build(BuildContext context){
      return InheritedProvder<T>{
         data: widget.data,
         child: widget.child,
      }
   }
}

实现了Model数据的改变自动带动了UI更新(类似于Android databinding思想方案),让我们精力能够更加注重业务代码. 再反过来我们业务中的代码,发现主动setState地方还是比较多,为后面项目重构优化提供了一个思路.