Flutter状态管理
在前面的文章中我们学习了Flutter中事件传递的方法,让我们可以在数据流向简单的业务场景中使用InheritedWidget、Notification 或者 EventBus。
但是随着业务逻辑的复杂,面对不同组件与不同页面之间的数据传递如果还使用前面讲道数据传递的方法就会显得异常繁琐,更会让页面的嵌套增多和数据流向的混乱,所以这个时候我们就需要有一种方案来管理我们需要跨界面传递的数据,于是便有了“状态管理”这个概念。
在前端开发中我们都会接触redux ,借助于redux 我们可以很轻松地完成多界面数据维护和获取,在Flutter中也有很多状态管理的第三方库,如Provider、Scoped Mode、flutter_redux、flutter_mobx 、BLoC、fish_redux等。
今天我们就先来看下Provider如何使用。
Provider
在前面的文章中我们学习过InheritedWidget的用法,通过对InheritedWidget的封装,使得Provider允许在 Widget 树中更加灵活地处理和传递数据。
Provider借助于ChangeNotifier实现发布者-订阅者模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class ChangeNotifier implements Listenable { List listeners=[]; @override void addListener(VoidCallback listener) { listeners.add(listener); } @override void removeListener(VoidCallback listener) { listeners.remove(listener); } void notifyListeners() { listeners.forEach((item)=>item()); }
... }
|
具体的细节我们不再具体去探讨,今天就来看看如何使用。
首先,我们假定这样一个场景,第一个界面显示用户的昵称,然后我们在第二个界面修改昵称再返回观察第一个界面的显示情况。
首先我们建立一个用户信息操作类UserInfoModel使它继承ChangeNotifier
1 2 3 4 5 6 7 8 9 10
| class UserInfoModel with ChangeNotifier { String _nickName = "userName"; String get nickName => _nickName; void updateNickName(String nickName) { _nickName=nickName; notifyListeners(); } }
|
数据更新
可以看到我们在UserInfoModel中定义了_nickName属性并设置相关回去与设置属性的方法,在设置属性方法中我们通过notifyListeners方法告知数据刷新。
因为Provider 是InheritedWidget实现的,所以数据也是有流向的,所以我们需要把ChangeNotifierProvider.value放在两个界面上面的位置,这样我们一旦更新一个页面的数据另外一个页面就也可以获取到。
首先,我们定一个入口
1 2 3 4 5
| void main() { runApp(new MaterialApp( home: MyApp(), )); }
|
然后定义入口Widget
1 2 3 4 5 6 7 8 9 10 11
| class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: UserInfoModel(), child: MaterialApp( home: FirstPage(), ) ); } }
|
第一个界面我们定义一个按钮和一个Text用来显示第二个界面更新的数据
我们使用context.watch()方法来获取到对象,并监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) {
return Scaffold( appBar: AppBar( title: Text("ProviderTitle"), ), body: Center( child: Column( children: [ Text( context.watch<UserInfoModel>().nickName), RaisedButton( child: Text("去设置界面"), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, ) ], ), ), ); } }
|
第二个界面我们定义一个输入框和一个按钮,点击按钮就把输入框的值设置给Provider
我们使用 Provider.of(context)方法来获取监听对象并进行修改操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { TextEditingController _unameController = TextEditingController(); var userModel= Provider.of<UserInfoModel>(context); return Scaffold( appBar: AppBar( title: Text("ProviderTitle"), ), body: Column( children: [ TextField( autofocus: true, controller: _unameController, decoration: InputDecoration( labelText: "用户名", hintText: "用户名或邮箱", prefixIcon: Icon(Icons.person)), ), RaisedButton( onPressed: () { userModel.updateNickName(_unameController.text);
}, child: Text("设置"), ) ], ), ); } }
|

同时管理多个数据
在上面我们介绍了如何通过Provider来管理用户名数据,那么如果涉及多个数据我们该如何来管理呢?
通常情况下我们可以把多个数据封装成一个完整的数据来进行操作,这种方法在数据见相互关联性比较接近的情况下是可以实现的,但是如何遇到数据关系不大的情况下还采用这种方法的话就会造成界面Widget不必要的重绘。
当然,Provider也为我们提供了解决方法,MultiProvider可以让我们同时管理多个数据。
还是以上面的例子来进行说明,我们在前面用户名的基础上又增加了一个“家庭地址”,在第一个界面新增一个Text用来显示家庭地址,在第二个界面新增一个输入框用来输入家庭地址。
首先我们定义一个用来管理地址的Model
1 2 3 4 5 6 7 8 9 10 11 12
| class UserLocationModel with ChangeNotifier { String _address = "address";
String get address => _address;
void setAddress(String address) { _address = address; notifyListeners(); } }
|
然后我们使用MultiProvider来管理多个Model
1 2 3 4 5 6 7 8 9 10
| class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider(providers: [ ChangeNotifierProvider.value(value: UserInfoModel()), ChangeNotifierProvider.value(value: UserLocationModel()) ], child: MaterialApp(home: FirstPage())); } }
|
然后在第一个界面接收并显示数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("ProviderTitle"), ), body: Center( child: Column( children: [ Text("用户名:${context.watch<UserInfoModel>().nickName}"), Text("家庭地址:${context.watch<UserLocationModel>().address}"), RaisedButton( child: Text("去设置界面"), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, ) ], ), ), ); } }
|
在第二个界面设置数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { TextEditingController _unameController = TextEditingController(); TextEditingController _homeController = TextEditingController(); var userModel = Provider.of<UserInfoModel>(context); var homeModel = Provider.of<UserLocationModel>(context); return Scaffold( appBar: AppBar( title: Text("ProviderTitle"), ), body: Column( children: [ TextField( autofocus: true, controller: _unameController, decoration: InputDecoration( labelText: "用户名", hintText: "用户名或邮箱", prefixIcon: Icon(Icons.person)), ),
RaisedButton( onPressed: () { userModel.updateNickName(_unameController.text); }, child: Text("设置用户名"), ), TextField( autofocus: true, controller: _homeController, decoration: InputDecoration( labelText: "家庭地址", hintText: "请输入家庭地址", prefixIcon: Icon(Icons.person)), ), RaisedButton( onPressed: () { homeModel.setAddress(_homeController.text);
}, child: Text("设置家庭地址"), ),
], ), ); } }
|

当然我们也可以使用Consumer2方法来获取多个数据的传递,这样就不需要再创建UserInfoModel和UserLocationModel了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("ProviderTitle"), ), body: Center( child: Consumer2<UserInfoModel, UserLocationModel>( builder: (context, UserInfoModel userInfoModel, UserLocationModel userLocationModel, _) => Column( children: [ Text("用户名:${userInfoModel.nickName}"), Text("家庭地址:${userLocationModel.address}"), RaisedButton( child: Text("去设置界面"), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, ) ], ))), ); } }
|
小结
- Provider是对InheritedWidget的封装方便我们在多个界面间传递数据
- Provider支持同时管理多个数据的状态
- 可以借助与Consumer-Consumer6方法来管理多个数据状态