前言
当年 React Native 正火的时候, 我撸了一个一席 https://github.com/evont/YixiReactNative 的客户端, 最近抽空把我自己的项目用 Flutter 写一下, 项目地址戳这里 https://github.com/evont/nowapp-flutter , 走过路过随手给个 star, 不胜感激; 以下是作为前端对 Flutter 的一些看法和经验的总结;
Dart
我在上手写 Flutter 的时候, 其实一开始并没有学习 Dart, 觉得有点类似 TypeScript,Dart 很好上手, 只在遇到一些不熟悉的问题时才去翻阅 Dart 文档, 说一下一些不一样的概念:
变量声明
var
在 JavaScript 和 Dart 中, 它都可以接受任意类型, 但 Dart 中 var 的变量一旦赋值, 类型便会确定, 则不能再改变其类型;
- var a;
- a = 'hello'; // a 已经确定为 String 类型
- a = 1; // 报错, 类型不能更改
dynamic & Object
JavaScript 中没有 dynamic 变量声明, 与 var 不同, 这两个都支持声明后改变变量类型, 但 Object 声明的变量只能使用 Object 所拥有的属性和方法, 而 dynamic 则支持所有属性
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 写法的讨论 https://github.com/flutter/flutter/issues/11609 , 对这种写法的吐槽, 最近在掘金沸点上看到一张很贴切的图:
关于 Widget 可以参考 Flutter 中文网的 Widget 目录 https://flutterchina.club/widgets/ , 具体的我就不展开写了, 下面讲讲一些不常见的需要注意的问题:
Expanded 不能用在不确定或者无限高度 Widget(如 SingleChildScrollView) 中
BuildContext 的概念
BuildContext 实际上是当前 Widget 所创建的 Element 对象, 在获取组件尺寸, 就需要用到 MediaQuery.of(context).size , 路由跳转时, 也要用到 Navigator.of(context), 比较详细的展开和理解说明可以参考深入理解 BuildContext 这篇文章;
Widget 的状态管理
这里要介绍一下 InheritedWidget,InheritedWidget 是一个特殊的 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 呢? 有两点原因:
状态访问问题
由于 build 方法在 state 每次改变时都会调用, 在 StatefulWidget 有很多状态时, build 方法需要传入一个 State 参数, 那么, 只能将 State 的所有状态公开才能在 State 类外部访问, 但公开状态后, 状态将不再具有私密性, 这样对状态的修改将变得不可控;
- Widget build(BuildContext context, State state){
- //state.a etc...
- ...
- }
继承 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 的生命周期如下图:
说一些常用的:
initState
这个函数相当于在 React 中的构造函数中初始化 State, 可以在这一步进行数据请求加载
didUpdateWidget
当调用了 setState 改变 Widget 状态时, Flutter 会创建一个新的 Widget 来绑定这个 State 并在此方法中传递旧 Widget , 如果你想比对新旧 Widget 并且对 State 做一些调整, 或者某些 Widget 上涉及到 controller 的变更时, 就可以在此回调方法中移除旧的 controller 并创建新的 controller;
- @override
- void didUpdateWidget(AVCycleLess oldWidget){
- super.didUpdateWidget(oldWidget);
- }
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 https://github.com/flutterchina/dio 这么一个优秀的请求库来简化我们的工作, 需要注意的是, 当 App 只有一个数据源时, Dio 应该使用单例模式
序列化
当我们获取到数据时, 通常我们都会拿到一个 JSON, 在 JavaScript 中, 我们可以很任意地直接使用点操作符来获取数据中的字段, 但是在 Dart 中, 你需要引入 dart:convert, 并使用 JSON.decode(JSON), 但它返回的是一个 Map<String, dynamic>, 意味着我们直到运行时才知道值的类型, 也就失去了大部分静态类型语言特性: 类型安全, 自动补全和最重要的编译时异常.
但这样一来, 我们的代码可能会变得非常容易出错. 我们通常需要编写模型类来序列化 JSON, 官方推荐了 json_serializable(相关操作看这里 https://flutterchina.club/json/ ) 来辅助我们生成库序列化 JSON, 通过这种方式, 我们就可以直接用点操作符来操作数据了.
如果还是嫌麻烦, 可以试试 https://github.com/debuggerx01/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 所学到的东西, 有些是查资料过程中看到的一些知识点, 并没有用在项目中, 还有很多细致的或者没遇到过的东西值得探讨, 等以后遇到了有机会再讲讲.
参考
Flutter 官网 https://docs.flutter.io/index.html (强烈建议以官方文档为准, 比较方便查询)
Flutter 实战 https://book.flutterchina.club/ (十分推荐)
Flutter 中文网 https://flutterchina.club/
来源: https://juejin.im/post/5c88a97451882501c950f0c9