当年React Native 正火的时候,我撸了一个一席的客户端,最近抽空把我自己的项目用Flutter 写一下,项目地址戳这里,走过路过随手给个star?,不胜感激; 以下是作为前端对Flutter 的一些看法和经验的总结;
我在上手写Flutter 的时候,其实一开始并没有学习Dart,觉得有点类似TypeScript,Dart 很好上手,只在遇到一些不熟悉的问题时才去翻阅Dart文档,说一下一些不一样的概念:
在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 中则是作为缩写语法的存在,两者的概念是不同的,应该区分清楚;
首先我们来看看同样的布局,使用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 目录,具体的我就不展开写了,下面讲讲一些不常见的需要注意的问题:
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'),
);
new Container(
color: Theme.of(context).accentColor,
chile: new Text(
'Text with a background color',
style: Theme.of(context).textTheme.title,
),
);
用过React 的都知道无状态组件和有状态组件,在Flutter中,StatelessWidget 便是无状态组件,它不依赖于除了传入的数据以外任何其他数据,意味着改变传入其构造函数的参数是改变其显示的唯一方式。而StatefulWidget 则是有状态组件,但是跟React有一点不同,在React 中,组件的render和state 是在一起的,而Flutter 中,StatefulWidget 需要重写createStae(),返回一个State,而build 方法需要放在State 中,至于为什么不放在StatefulWidget 呢?有两点原因:
Widget build(BuildContext context, State state){
//state.a etc...
...
}
class ChildWidgert extends ParentWidget{
@override
Widget build(BuildContext context, State state){
super.build(context, _parentWidgetState)
}
}
initState
这个函数相当于在React 中的构造函数中初始化State,可以在这一步进行数据请求加载
didUpdateWidget
当调用了 setState 改变Widget 状态时,Flutter 会创建一个新的 Widget 来绑定这个 State 并在此方法中传递旧 Widget ,如果你想比对新旧 Widget 并且对 State 做一些调整,或者某些 Widget 上涉及到 controller 的变更时,就可以在此回调方法中移除旧的 controller 并创建新的 controller;
@override
void didUpdateWidget(AVCycleLess oldWidget){
super.didUpdateWidget(oldWidget);
}
当Widget 被释放(如路由切换),Widget 中存在一些监听或持久化的变量,你就需要在 dispose 中进行释放。
当我们进入页面进行一些耗时的操作,比如请求数据、初始化某些设置等时,我们通常需要显示一个加载页面,一般做法都是判断数据状态来切换显示的组件,而在Flutter 中则有FutureBuilder 这种便利的解决方案
在Flutter 中,路由分为静态路由和动态路由,静态路由无法传递参数,所以在需要传递参数的情况下只能使用动态路由;
静态路由在新建App 时定义,使用Navigator.of(context).pushNamed(’/router/a’);进行切换,pushNamed 返回一个Future,可以接收来自下一个页面的返回值。
return new MaterialApp(
home: new Text('hello'),
routes: {
'/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');
}));
在Flutter 中,网络请求是由HttpClient 进行的,但其操作十分麻烦,所以有Dio 这么一个优秀的请求库来简化我们的工作,需要注意的是,当App 只有一个数据源时,Dio 应该使用单例模式
当我们获取到数据时,通常我们都会拿到一个json,在JavaScript 中,我们可以很任意地直接使用点操作符来获取数据中的字段,但是在Dart中,你需要引入dart:convert,并使用JSON.decode(json),但它返回的是一个Map
但这样一来,我们的代码可能会变得非常容易出错。我们通常需要编写模型类来序列化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都可以接收到事件
}
在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 ,相信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 中是有以下使用条件的:
在使用Tab 时,切换Tab后,每个Tab 都会被销毁然后重建,于是会多次调用initState,那有没有类似Vue 中的 组件一样的存在呢?答案是有的,那就是AutomaticKeepAliveClientMixin。只需要继承这个mixin并实现wantKeepAlive 方法即可。但widget在不显示之后也不会被销毁仍然保存在内存中,所以慎重使用这个方法
class APageState extends State with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
// ...
}
Android架构师的门槛,有没有免费学习资料?