一个前端码农的 Flutter 实战经验

14,165 阅读11分钟

前言

当年React Native 正火的时候,我撸了一个一席的客户端,最近抽空把我自己的项目用Flutter 写一下,项目地址戳这里,走过路过随手给个star🌟,不胜感激; 以下是作为前端对Flutter 的一些看法和经验的总结;


Dart

我在上手写Flutter 的时候,其实一开始并没有学习Dart,觉得有点类似TypeScript,Dart 很好上手,只在遇到一些不熟悉的问题时才去翻阅Dart文档,说一下一些不一样的概念:

  • 变量声明

    1. var

      在JavaScript 和Dart 中,它都可以接受任意类型,但Dart中var的变量一旦赋值,类型便会确定,则不能再改变其类型;

      var a;
      a = 'hello'; // a 已经确定为String类型
      a = 1; // 报错,类型不能更改
      
    2. dynamic & Object

      javaScript中没有dynamic 变量声明,与var 不同,这两个都支持声明后改变变量类型,但Object 声明的变量只能使用Object所拥有的属性和方法,而dynamic 则支持所有属性

    3. final & const

      从字面上可以看出这两个都是声明常量,但是const 变量是编译时常量,而final 变量则在第一次使用时初始化;

  • 异步支持

    在Javascript 和Dart中都有相同用法的async、await,但没有Promise,取而代之的是Future,但没有resolve 和reject

  • 构造函数 在Dart 中,子类不会继承父类的命名构造函数。如果不显式提供子类的构造函数,系统就提供默认的构造函数。同时,写法也变得更简洁;

        class Point {
          num x;
          num y;
    
          Point(this.x, this.y);// 这句等同于
          /* 
          Point(num x, num y) {
            this.x = x;
            this.y = y;
          }
          */
        }
    
  • 箭头函数

    在Javascript 中,箭头函数是作为一个影响this 作用域等的存在,但在Dart 中则是作为缩写语法的存在,两者的概念是不同的,应该区分清楚;


UI 布局

首先我们来看看同样的布局,使用HTML + CSS 和Flutter 的写法区别

在Flutter 中,一切UI 都基于Widget,在上图中,Container 便是一个Widget,靠style 来设置样式(也可以使用Theme,后文中细讲),子类嵌套在child 中,。

class MainApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

实际上这种写法有点类似虚拟Dom,以树形嵌套来编写,但是这种写法个人觉得维护起来很要命,如果没有足够细分组件的话,可读性也会变得很差,实际上,Flutter 的issues 中也有关于类JSX 写法的讨论,对这种写法的吐槽,最近在掘金沸点上看到一张很贴切的图:

关于Widget 可以参考Flutter 中文网的Widget 目录,具体的我就不展开写了,下面讲讲一些不常见的需要注意的问题:

  1. Expanded 不能用在不确定或者无限高度Widget(如SingleChildScrollView) 中

  2. BuildContext 的概念

    BuildContext 实际上是当前Widget 所创建的Element对象,在获取组件尺寸,就需要用到MediaQuery.of(context).size ,路由跳转时,也要用到Navigator.of(context),比较详细的展开和理解说明可以参考深入理解BuildContext 这篇文章;

  3. Widget 的状态管理

    这里要介绍一下InheritedWidgetInheritedWidget是一个特殊的Widget,你可以将其作为另一个子树的父级放在Widgets树中。该子树的所有子Widget 都能与该InheritedWidget 公开的数据进行交互,从而实现了Widget 间的通信;更多状态管理的方式可以参考 深入探索 flutter 中的状态管理方式

样式

在Flutter 中,样式并没有抽离出来,而是以各种(混乱甚至有点怪异)组合的方式来使用,设置文本要用TextStyle,设置边框背景等要用decoration,感兴趣的可以看看样式的一些用法对比

这里要吐槽一下样式的管理,在Flutter 中,可以使用Theme来共享样式,但是单个Widget 的样式除了DefaultTextStyle设置默认文本样式外没得继承,还是要自己一个个写,这里就推动了对组件进行细化(不然懒得重复写),主题有以下使用方式

  • 全局主题

    new MaterialApp(
      title: title,
      theme: new ThemeData(
          brightness: Brightness.dark,
      ),
    );
    
  • 局部主题

    new Theme(
      data: new ThemeData(
          accentColor: Colors.yellow,
      ),
      child: new Text('Hello World'),
    );
    
  • 拓展主题

    如果你不想覆盖所有的样式,可以继承App的主题,只覆盖部分样式,使用copyWith方法。

    new Theme(
      data: Theme.of(context).copyWith(accentColor: Colors.yellow),
      child: new Text('extend theme'),
    );
    
  • 获取主题

    Theme.of(context) 会查找Widget 树,并返回最近的一个Theme对象。如果父层级上有Theme对象,则返回这个Theme,如果没有,就返回App的Theme。创建好主题,只要在Widget的构造方法里面通过Theme.of(context) 方法来调用。

    new Container(
      color: Theme.of(context).accentColor,
      chile: new Text(
          'Text with a background color',
          style: Theme.of(context).textTheme.title,
      ),
    );
    

状态组件

Stateful 与StateLess

用过React 的都知道无状态组件和有状态组件,在Flutter中,StatelessWidget 便是无状态组件,它不依赖于除了传入的数据以外任何其他数据,意味着改变传入其构造函数的参数是改变其显示的唯一方式。而StatefulWidget 则是有状态组件,但是跟React有一点不同,在React 中,组件的render和state 是在一起的,而Flutter 中,StatefulWidget 需要重写createStae(),返回一个State,而build 方法需要放在State 中,至于为什么不放在StatefulWidget 呢?有两点原因:

  1. 状态访问问题

    由于build 方法在state 每次改变时都会调用,在StatefulWidget有很多状态时,build 方法需要传入一个State 参数,那么,只能将State的所有状态公开才能在State类外部访问,但公开状态后,状态将不再具有私密性,这样对状态的修改将变得不可控;

    Widget build(BuildContext context, State state){
      //state.a etc...
      ...
     }
    
  2. 继承StatefulWidget问题

    当第一个情况发生后,如果有个子Widget 继承自一个引入了抽象方法build(BuildContext context)的父Widget,那么子Widget 在实现这个build 时都需要传入一个state,此时父Widget 就必须将自己的state 传入给子Widget,这样就十分不合理,因为父Widget 的state 只与自身逻辑有关,且传递给子Widget 还需另外的传递机制,因此,应该将build 方法放在State 中。

      class ChildWidgert extends ParentWidget{
         @override
         Widget build(BuildContext context, State state){
          super.build(context, _parentWidgetState)
         }
      }
    

生命周期

Flutter 的生命周期如下图:

说一些常用的:

  1. initState

    这个函数相当于在React 中的构造函数中初始化State,可以在这一步进行数据请求加载

  2. didUpdateWidget

    当调用了 setState 改变Widget 状态时,Flutter 会创建一个新的 Widget 来绑定这个 State 并在此方法中传递旧 Widget ,如果你想比对新旧 Widget 并且对 State 做一些调整,或者某些 Widget 上涉及到 controller 的变更时,就可以在此回调方法中移除旧的 controller 并创建新的 controller;

    @override
    void didUpdateWidget(AVCycleLess oldWidget){
      super.didUpdateWidget(oldWidget);
    }
    
  3. dispose

    当Widget 被释放(如路由切换),Widget 中存在一些监听或持久化的变量,你就需要在 dispose 中进行释放。

FutureBuilder

当我们进入页面进行一些耗时的操作,比如请求数据、初始化某些设置等时,我们通常需要显示一个加载页面,一般做法都是判断数据状态来切换显示的组件,而在Flutter 中则有FutureBuilder 这种便利的解决方案,这里展开篇幅会很长,可以参考FutureBuilder的使用方法和注意事项

路由

在Flutter 中,路由分为静态路由和动态路由,静态路由无法传递参数,所以在需要传递参数的情况下只能使用动态路由;

静态路由

静态路由在新建App 时定义,使用Navigator.of(context).pushNamed('/router/a');进行切换,pushNamed 返回一个Future,可以接收来自下一个页面的返回值。

return new MaterialApp(
    home: new Text('hello'),
    routes: <String, WidgetBuilder> {
        '/router/a': (_) => new APage(),
        '/router/b': (_) => new BPage(),
    },
);
// then 说明
// 当前页面
Navigator.of(context).pushNamed('/router/b').then((value) {
    // value 为下一个页面的返回值
});
// b 页面
Navigator.of(context).pop('some data');

动态路由

动态路由使用push方法,传入一个route 对象,在builder 中创建一个新页面对象,如果需要自定义动画效果,只需要使用PageRouteBuilder 替换MaterialPageRoute ,在transitionsBuilder 中定义动画即可。

Navigator.of(context).push(new MaterialPageRoute(builder: (_) {
    return new NewPage(data: 'some data');
}));

网络请求

Dio

在Flutter 中,网络请求是由HttpClient 进行的,但其操作十分麻烦,所以有Dio 这么一个优秀的请求库来简化我们的工作,需要注意的是,当App 只有一个数据源时,Dio 应该使用单例模式

序列化

当我们获取到数据时,通常我们都会拿到一个json,在JavaScript 中,我们可以很任意地直接使用点操作符来获取数据中的字段,但是在Dart中,你需要引入dart:convert,并使用JSON.decode(json),但它返回的是一个Map<String, dynamic>,意味着我们直到运行时才知道值的类型,也就失去了大部分静态类型语言特性:类型安全、自动补全和最重要的编译时异常。

但这样一来,我们的代码可能会变得非常容易出错。我们通常需要编写模型类来序列化JSON,官方推荐了json_serializable(相关操作看这里) 来辅助我们生成库序列化JSON,通过这种方式,我们就可以直接用点操作符来操作数据了。

如果还是嫌麻烦,可以试试JSONFormat4Flutter这一工具(我还没用过,看着很不错的样子。)


事件处理

在Vue 中,我们只需要使用@click 之类的方法即可监听事件,而React 中则是onClick之类的方法,但在Flutter 中,我们需要将需要监听事件的元素包裹在GestureDetector 中,使用onTap 等方法来处理事件,对事件的行为表现,我们可以通过设置behavior来控制,

enum HitTestBehavior {
  deferToChild, // 子widget会一个接一个的进行命中测试,如果子Widget中有测试通过的,则当前Widget通过,这就意味着,如果指针事件作用于子Widget上时,其父(祖先)Widget也肯定可以收到该事件。
  opaque,// 在命中测试时,将当前Widget当成不透明处理(即使本身是透明的),最终的效果相当于当前Widget的整个区域都是点击区域
  translucent,// 当点击Widget透明区域时,可以对自身边界内及底部可视区域都进行命中测试,这意味着点击顶部widget透明区域时,顶部widget和底部widget都可以接收到事件
}

Canvas

在Flutter 中,如果需要使用Canvas,我们需要继承CustomPainter 并重写paint方法来绘制自定义图形。在使用Canvas时,我们需要知道三个概念:

  • canvas

    画布对象,包括了各种绘制方法,用来绘制各种图形

  • size

    当前绘制区域的大小

  • paint

    画笔,用来控制画出来的各种属性,如颜色、描边及抗锯齿等;

使用例子如下:

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
      canvas.drawRect(Offset.zero & size, Paint()
      ..isAntiAlias = true // 抗锯齿
      ..style = PaintingStyle.fill // 填充,stroke则为使用描边
      ..color = Color(0xFF000000) // yanse
      );
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false; // 强制不重绘,提高性能
}

复用

Mixin

说到mixin ,相信Vue 和React 的使用者都很熟悉,虽然React中mixin已 被高阶函数或Decorator取代,但在Flutter 中,mixin 还是得以保留。 它使用with 来引入一个mixin,定义的方式如下:

class A {
  int a = 1;
  void b(){
    print('c');
  }
}

class B with A{

}
B b = new B();
print(b.a);
b.b();

不过,mixin 在 Dart 中是有以下使用条件的:

  • mixins类只能继承自object
  • mixins类不能有构造函数
  • 一个类可以mixins多个mixins类
  • 可以mixins多个类,不破坏Flutter的单继承

Keep-alive

在使用Tab 时,切换Tab后,每个Tab 都会被销毁然后重建,于是会多次调用initState,那有没有类似Vue 中的<keep-alive> 组件一样的存在呢?答案是有的,那就是AutomaticKeepAliveClientMixin。只需要继承这个mixin并实现wantKeepAlive 方法即可。但widget在不显示之后也不会被销毁仍然保存在内存中,所以慎重使用这个方法

class APageState extends State<APage> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  // ...
}

后话

以上只是我这10天断断续续做出第一个粗糙的Flutter App所学到的东西,有些是查资料过程中看到的一些知识点,并没有用在项目中,还有很多细致的或者没遇到过的东西值得探讨,等以后遇到了有机会再讲讲。


参考