上一课时我详细介绍了有/无状态组件的应用设计,但是在设计过程中,还缺乏一个对状态管理的考虑。本课时介绍状态管理设计的必要性,以及一些常见的状态管理技术对比,最后再着重通过 Provider 来优化前一课时中的例子。
状态管理场景
上一课时的例子中,只涉及一个有状态的组件 article_like_bar ,接下来我们需要实现另外一个详情页面,并且在详情页面中也需要一个点赞功能,具体的界面效果可以参考动图 1 (为了界面更好,我在上一课时的基础上增加了一些样式)。
图 1 增加二级点赞详情页面效果
在上面的动图例子中,你是否发现了一个问题?第一个页面的点赞数与第二个页面的点赞数并不同步。在实际项目开发过中,需求方希望二级详情页面的点赞数能与第一个页面的点赞数同步。
如果不引入新的技术方案,能想到的办法就是将该状态进行提升,放到其共同的父节点上,然后将父节点设计为有状态组件,并提供修改状态的方法给到子组件。可以用图 2 来表示。
图 2 状态提升共享方式
上面的方式是可以做到这点,但是你有没有发现,只因为一个点赞行为,就需要将两个页面的所有组件(静态组件和动图组件)进行重新 build ,成本实在太高,这也违背了我们上一课时的组件设计原则(尽可能减少动态组件下的静态组件)。为了更好地解决这个问题,我们就需要引入一些状态管理的方法,下面就介绍一些常见的技术方案,同时做一个对比。
状态选型对比
状态管理技术不少于 10 种,但是为了高效,我只介绍其中比较核心的三个,第一个是原生所使用的 InheritedWidget ;第二个是相对前端同学比较熟悉的 Redux 技术;最后一个则是我们推荐使用的技术 Provider 。
InheritedWidget
InheritedWidget 核心原理和状态提升原理一致,将 likeNum 提升到根节点,但不需要一层层地将变量传递下去,只需要在根节点声明即可。
现在我们有一个页面,页面下有两个组件,两个组件都需要用同一个名字,并且第二个组件的名字可以点击切换随机名字,而切换以后需要及时更新第一个组件中的名字。页面效果如图 3 所示。
图 3 多组件状态共享效果
按照上面介绍的例子以及上一课时的知识点,画一个简单的组件树,并且附带上需要的状态属性,如图 4 所示。
图 4 InheritedWidget 组件设计
-
首先创建一个根结点为一个有状态组件 name_game;
-
name_game 为一个有状态类,状态属性为 name,并带有 changName 的状态修改方法;
-
- 创建一个状态管理类组件 NameInheritedWidget ;
-
创建 NameInheritedWidget 的三个子组件,分别为 welcome(显示欢迎 name )、random_name(显示 name ,并且有点击切换随机 name 操作)和 other_widgets 。
对于上面的结构,肯定有很多同学比较疑惑,other_widgets 并没有使用这个 name 状态,为什么要在 NameInheritedWidget 下呢?
带着这样的疑惑,我们先来看下 name_game 核心代码(为了在专栏中更简洁,我省去了部分代码,完整代码大家可以参考文章下的 github 代码地址)。
/// 随机名字游戏组件状态管理类
class NameGameState extends State<NameGame> {
/// name 状态
String name;
/// 构造函数参数,避免父组件状态变化,而引起的子组件的重 build 操作
Widget child;
/// 修改当前名字
void changeName() {
List<String> nameList = ['flutter one', 'flutter two', 'flutter three'];
int pos = Random().nextInt(3);
setState(() {
name = nameList[pos];
});
}
@override
void initState() {
setState(() {
name = 'test flutter';
});
super.initState();
}
/// 构造函数
NameGameState()
{
child = Column (
children: <Widget>[
Welcome(),
RandomName(),
TestOther(),
]
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
NameInheritedWidget(
child: child,
onNameChange: changeName,
name: name
),
],
);
}
}
上面代码中,定义状态属性 name ,并创建了可以修改 state 的 changeName 方法。接下来在 build 中使用 NameInheritedWidget 这个组件(该组件可以理解为前端所说的高阶组件,也就是通过将组件作为参数传递进该组件,并返回一个新的组件的功能组件),这个组件包裹了两个需要状态 name 的组件( Welcome 和 RandomName )以及一个不需要状态的 TestOther。
上面代码中还有一个比较特殊的地方,就是将 child 作为了 state ,在构造函数中进行了定义,并将该组件的所有子组件都包含在了 child 中