一个前端码农的 Flutter 实战经验
前言
当年React Native 正火的时候,我撸了一个一席的客户端,最近抽空把我自己的项目用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(),
);
}
}

关于Widget 可以参考Flutter 中文网的Widget 目录,具体的我就不展开写了,下面讲讲一些不常见的需要注意的问题:
样式
这里要吐槽一下样式的管理,在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 这么一个优秀的请求库来简化我们的工作,需要注意的是,当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所学到的东西,有些是查资料过程中看到的一些知识点,并没有用在项目中,还有很多细致的或者没遇到过的东西值得探讨,等以后遇到了有机会再讲讲。
参考
- Flutter 官网(强烈建议以官方文档为准,比较方便查询)
- Flutter实战 (十分推荐)
- Flutter 中文网
转载自:https://juejin.cn/post/6844903796317880327