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

前言

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


Dart

我在上手写Flutter 的时候,其实一开始并无学习Dart,以为有点相似TypeScript,Dart 很好上手,只在遇到一些不熟悉的问题时才去翻阅Dart文档,说一下一些不同的概念:前端

  • 变量声明java

    1. vargit

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

      var a;
      a = 'hello'; // a 已经肯定为String类型
      a = 1; // 报错,类型不能更改
      复制代码
    2. dynamic & Objectjson

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

    3. final & const安全

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

  • 异步支持网络

    在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所学到的东西,有些是查资料过程当中看到的一些知识点,并无用在项目中,还有不少细致的或者没遇到过的东西值得探讨,等之后遇到了有机会再讲讲。


参考

相关文章
相关标签/搜索