利用 Flutter 内置的许多控件我们可以打造出一款不仅漂亮而且完美跨平台的 App 外壳,我利用其特性完成了类似 知乎App的UI界面 ,然而一款完整的应用程序显然不止有外壳这么简单。填充在外壳里面的是数据,数据来源或从本地,或从云端,大量的数据处理很容易造成数据的混乱,耦合度提高,不便于维护,于是诞生了很多设计模式和状态管理的方式。
目前 Flutter 常用状态管理方式有如下几种:
- ScopedModel
- BLoC (Business Logic Component) / Rx
- Redux
这篇文章暂且不提这些比较复杂的模式。我们简单的提出三个问题:
- Flutter 中组件之间如何通信?
- 更新 State 后组件以何种方式重新渲染?
- 如何在路由转换之间保持状态同步?
初探 State
我以创建新项目 Flutter 给我们默认的计数器应用为例,通过路由我将其拆分为两部分
MyHomePage
和
PageTwo
,
MyHomePage,持有一个
_counter
变量和一个增加计数的方法,PageTwo,接收两个参数(计数的至和增加计数的方法):
class PageTwo extends StatefulWidget {
final int count;
final Function increment;
const PageTwo({Key key, this.count, this.increment}) : super(key: key);
_PageTwoState createState() => _PageTwoState();
}
class _PageTwoState extends State<PageTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Page Two"),
),
body: Center(
child: Text(widget.count.toString(), style: TextStyle(fontSize: 30.0),),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: widget.increment,
),
);
}
}
复制代码
出现的状况是:我们在首页点击按钮触发计数器增加,路由到 PageTwo 后,数值正常显示,然而点击这个界面中的 add 按钮该页面的数值并未发生改变,通过观察父页面的 count 值确实发生了改变,因此再次通过路由到第二个界面界面才显示正常。解答上面三个问题:
-
Flutter 中组件之间如何通信?
参数传递。
-
更新 State 后组件以何种方式重新渲染?
只渲染当前的组件(和子组件,这里暂未证明,但确实是触发 SetSate() 后,其所有子组件都将重新渲染。)
-
如何在路由转换之间保持状态同步?
父组件传递状态值到子组件,子组件拿到并显示,但却不能实时更改😀,我一时半会还正没想出什么解决方法,我相信即使能做到也不优雅。
证明触发 SetSate() 后,其所有子组件都将重新渲染:我在副组件中添加两个子组件,一旦触发渲染变打印相关数据:
TestStateless(),
TestStateful()
class TestStateless extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('build TestStateless');
return Text('TestStateless');
}
}
class TestStateful extends StatefulWidget {
@override
_TestStatefulState createState() => _TestStatefulState();
}
class _TestStatefulState extends State<TestStateful> {
@override
Widget build(BuildContext context) {
print('build TestStateful');
return Text('_TestStatefulState');
}
}
复制代码
此时到 PageTwo 触发 add 事件,日志出来:
通过这种简单的方式已经可以说明一个问题,即以最简单的方式我们已经可以完成状态传递和组件渲染,而路由间保持状态一致还不能解决。
InheritedWidget
Google 官方给我们的解决方案是
InheritedWidget
,怎么理解他,我们可以称它为“状态树”,它使得所有的 widget 的 State 来源统一,这样一旦有一处触发状态改变,Flutter 以某种方式感应到了(有个监听器),砍掉它,长出一个新树,Perfect!所有地方都能感受到他的变化。上面提到的第一种状态管理方式
ScopedModel
便是基于此而产生的一套第三方库。
其实现在看来 InheritedWidget 已经非常简单了,我们抓住两个点即可完全掌握它:
-
状态树中的数据
class MyInheritedValue extends InheritedWidget { const MyInheritedValue({ Key key, @required this.value, @required Widget child, }) : assert(value != null), assert(child != null), super(key: key, child: child); final int value; static MyInheritedValue of(BuildContext context) { return context.inheritFromWidgetOfExactType(MyInheritedValue); } @override bool updateShouldNotify(MyInheritedValue old) => value != old.value; } 复制代码
注入到根组件中:
Widget build(BuildContext context) { return MyInheritedValue( value: 42, child: ... ); } 复制代码
-
使用状态树中数据的其他 Widget
// 拿到状态树中的值 MyInheritedValue.of(context).value 复制代码
请注意:这种情况下是不能改 InheritedWidget 中的值的,需要改也很简单就是将 MyInheritedValue 的值封装成一个对象,每次改变这个对象的值,具体法相看我的 样例代码 !
上面所说砍掉整棵树过于粗暴却并不夸张,因为一处改变它将联动整棵树,
ScopedModel 是基于 InheritedWidget 的库,实现起来与 InheritedWidget 大同小异,而且其有一种可以让局部组件不改变的方式:设置 rebuildOnChange 为 false。
return ScopedModelDescendant<CartModel>(
rebuildOnChange: false,
builder: (context, child, model) => ProductSquare(
product: product,
onTap: () => model.add(product),
),
);
复制代码
具体代码请看 GitHub,ScopedModel 样例截取一个老外给的实例,就是下方参考链接 Google 开发者大会上演讲的那两位其中之一。
这种方式显然有点不足之处就是一旦遇到小规模变动就要引起大规模重新渲染,所以当项目达到一定的规模考虑 Google 爸爸给我们的另一种解决方案。
Streams(流)
在 Android 开发中我们经常会用到 RxJava 这类响应式编程方法的框架,其强大之处无须多言,而 Stream 看上去就是在 Dart 语言中的响应式编程的一种实现。
-
Streams 是什么鬼?
如果要具体把 Streams 说清楚,一篇文章绝对不够,这里先介绍一下其中的概念,这篇文章目的就是如此。待我后续想好怎么具体描述清楚。