状态管理是 Flutter 中一个非常火热的话题。随着应用程序规模的逐渐扩大,业务逻辑也随之变得复杂,比如在用户登陆后,应用各个页面的状态都需要得到及时的更新。传统的 UI 和业务逻辑纠缠在一起的做法,使得代码变得复杂冗余,也为各种潜在 bug 埋下了伏笔。因此将状态和业务逻辑分离,统一地对状态进行管理,使得编程实现和应用结构都更加清晰简明。在 Flutter 的官方文档中也对状态管理进行了讨论,列举了 Flutter 中几种常见的状态管理的框架。
ScopedModel
scoped_model 是最为简单的 Flutter 状态管理框架,它提供了三个类:
- Model:所有我们自定义的模型都需要继承自这个 Model。当 Model 的状态发生改变时,需要在 Model 中通过
notifyListeners()
方法来通知所有的 listener。 - ScopedModel:当把一个 Widget 和 Model 包裹在一个 ScopedModel 中时,这个 Widget 的所有子 Widget 都可以访问到这个 Model。
- ScopedModelDescendant:用 ScopedModel 包裹的 Widget 的子 Widget 都应该用ScopedModelDescendant 来包裹,这样这些子 Widget 就可以获得第一步中的 Model,并且在 Model 状态发生改变后自动重建。
一个完整的例子可以在 Github 上进行查看。
Redux
Redux 原本是用于前端开发的状态管理框架,但也可以和 Flutter 很好地结合起来。在 Redux 中,有四大重要的概念:State、Action、Reducer 以及 Store。State 就是应用程序中的状态,State 会反映在 UI 上。Action 是应用程序中的动作,该动作会引起 State 的改变。而 Reducer 则是用来处理 Action 的,它接受一个 Action 和一个 State,并根据 Action 的内容返回一个新的 State。Store 既是一个状态容器,也是一个动作派发者,应用程序当前的状态可以通过 store.getState
来获得,新的 Action 则通过 store.dispatch(action)
派发给 Reducer 进行处理。
在 Flutter 生态中,Redux 包提供了对Store
、Middleware
、Reducer
等的支持,而 flutter_redux 插件则提供了StoreProvider
、StoreConnector
、StoreBuilder
几个让使用 Redux 更加便利的类,让我们可以更容易地在 Flutter Widget 中获得 Store 及其相关状态,以及进行 Action 的派发。
StoreProvider
的作用是将 Store 传入 App 中,这样整个 App 内的 Widget 都可以通过 StoreConnector
或者StoreBuilder
来获得 Store:
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 |
void main() { final store = new Store<GlobalState>( globalReducer, initialState: GlobalState.initial(), middleware: [thunkMiddleware], ); runApp(MyApp( store: store, )); } class MyApp extends StatefulWidget { final Store<GlobalState> store; MyApp({ Key key, this.store, }) : super(key: key); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return StoreProvider<GlobalState>( store: widget.store, child: MaterialApp( title: 'Flutter Weather', home: WeatherPage(), ), ); } } |
StoreConnector
和 StoreBuilder
的作用都是让子 Widget 获得 Store,并通过 Store.getState 方法获得 State,然后通过 State 来设置子 Widget 的样式。不同的是,StoreConnector
在使用时需要传入一个 ViewModel 类型,并且需要编写 convert 方法,来确定如何从 Store 创建 ViewModel,而StoreBuilder
则是直接使用就可以了。两者适用于不同的场景:当State
结构复杂,而当前的 Widget 只需要 State 中的某些属性时,那么可以使用StoreConnector
,并将需要用到的这些属性映射到一个 ViewModel 里,这样所需的属性直接从 ViewModel 里取就可以了,避免了类似于store.state.weatherState.weather.condition
这样复杂冗长的取值过程。而当State
比较简单时,就可以直接使用StoreBuilder
。
如下是使用StoreConnector
的一段示例:
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 50 51 52 53 54 55 56 57 58 59 60 |
class CombinedWeatherTemperature extends StatelessWidget { final model.Weather weather; CombinedWeatherTemperature({ Key key, @required this.weather, }) : assert(weather != null), super(key: key); @override Widget build(BuildContext context) { return StoreConnector<GlobalState, ViewModel>( converter: (store) { return ViewModel(temperatureUnits: store.state.settingsState.temperatureUnits); }, builder: (BuildContext context, ViewModel vm) { return Column( children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: EdgeInsets.all(20.0), child: WeatherConditions( condition: weather.condition, ), ), Padding( padding: EdgeInsets.all(20), child: Temperature( temperature: weather.temp, high: weather.maxTemp, low: weather.minTemp, units: vm.temperatureUnits, ), ) ], ), Center( child: Text( weather.formattedCondition, style: TextStyle( fontSize: 30, fontWeight: FontWeight.w200, color: Colors.white, ), ), ) ], ); }, ); } } class ViewModel { TemperatureUnits temperatureUnits; ViewModel({@required this.temperatureUnits}) : assert(temperatureUnits != null); } |
当使用StoreBuilder
来构建时,其写法是非常类似的:
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 50 51 52 |
class CombinedWeatherTemperature extends StatelessWidget { final model.Weather weather; CombinedWeatherTemperature({ Key key, @required this.weather, }) : assert(weather != null), super(key: key); @override Widget build(BuildContext context) { return StoreBuilder( builder: (BuildContext context, Store<GlobalState> store) { return Column( children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: EdgeInsets.all(20.0), child: WeatherConditions( condition: weather.condition, ), ), Padding( padding: EdgeInsets.all(20), child: Temperature( temperature: weather.temp, high: weather.maxTemp, low: weather.minTemp, units: store.state.settingsState.temperatureUnits, ), ) ], ), Center( child: Text( weather.formattedCondition, style: TextStyle( fontSize: 30, fontWeight: FontWeight.w200, color: Colors.white, ), ), ) ], ); }, ); } } |
Bloc
Bloc(Business Logic Component)是由来自谷歌的 Paolo Soares 和 Cong Hui 设计的设计模式,最早在 2018 年的 DartConf 上被展示。这个模式的设计目的是将数据的展示和业务逻辑分离开,使得测试和重用变得更加容易。
Flutter 中的 bloc 和 flutter_bloc 插件让使用 Bloc 和使用 Redux 来进行状态管理有相似的流程,因此对于熟悉 Redux 的人来说,可以很快速地切换到 Bloc 上。
flutter_bloc 中提供了如下几个 Widget:
- BlocBuilder
- BlocProvider和BlocProviderTree
- BlocListener和BlocListenerTree
BlocProvider
和 StoreProvider
的作用相同,当用 BlocProvider 来构建一个 Widget 后,它的所有子 Widget 也可以取到其中的 bloc。BlocBuilder 则类似于 Flutter 中的 StreamBuilder,它有一个接受 bloc 的参数,还有一个接受 State 的 builder 函数,当 State 发生变化时,build 函数会自动被调用。BlocListener 与 BlocBuilder 也很类似, 有一个接受 bloc 的参数,还有一个接受 State 的 listener 函数,当 State 发生变化时,listener 函数会被调用。
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 |
class MyApp extends StatefulWidget { final WeatherRepository weatherRepository; MyApp({Key key, @required this.weatherRepository}) :assert(weatherRepository != null), super(key: key); @override State<MyApp> createState() => _AppState(); } class _AppState extends State<MyApp> { ThemeBloc _themeBloc = ThemeBloc(); SettingsBloc _settingsBloc = SettingsBloc(); @override Widget build(BuildContext context) { return BlocProviderTree( blocProviders: [ BlocProvider<ThemeBloc>(bloc: _themeBloc,), BlocProvider<SettingsBloc>(bloc: _settingsBloc,), ], child: BlocBuilder( bloc: _themeBloc, builder:(_, ThemeState themeState) { return MaterialApp( title: 'Flutter Weather', theme: themeState.theme, home: Weather( weatherRepository: widget.weatherRepository, ), ); } ) ); } @override void dispose() { _themeBloc.dispose(); _settingsBloc.dispose(); super.dispose(); } } |
在子 Widget 中,可以通过 BlocProvider 的 of 方法来获得 bloc:
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 50 51 |
class CombinedWeatherTemperature extends StatelessWidget { final model.Weather weather; CombinedWeatherTemperature({ Key key, @required this.weather, }) : assert(weather != null), super(key: key); @override Widget build(BuildContext context) { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: EdgeInsets.all(20.0), child: WeatherConditions(condition: weather.condition), ), Padding( padding: EdgeInsets.all(20.0), child: BlocBuilder( bloc: BlocProvider.of<SettingsBloc>(context), builder: (_, SettingsState state) { return Temperature( temperature: weather.temp, high: weather.maxTemp, low: weather.minTemp, units: state.temperatureUnits, ); }, ) ), ], ), Center( child: Text( weather.formattedCondition, style: TextStyle( fontSize: 30, fontWeight: FontWeight.w200, color: Colors.white, ), ), ), ], ); } } |
在上面提到的 bloc,与 Redux 的 Store 作用类似。bloc 需要继承自 Bloc 类,并且重载mapEventToState
方法,该方法类似于 Redux 中的 Reducer,根据当前的 State 与 Event 来确定下一步的 State。Event 就类似于 Redux 中的 Action。bloc 中也有 dispatch 方法,参数是一个 Event:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> { @override SettingsState get initialState => SettingsState(temperatureUnits: TemperatureUnits.celsius); @override Stream<SettingsState> mapEventToState(SettingsEvent event) async* { if (event is TemperatureUnitsToggled) { yield SettingsState( temperatureUnits: currentState.temperatureUnits == TemperatureUnits.celsius ? TemperatureUnits.fahrenheit:TemperatureUnits.celsius, ); } } } |
为了比较 Redux 和 Bloc 的不同,我在学习了 Bloc 文档中提供的 Weather 应用编写教程后,将它用 Redux 重新实现,所有代码已经上传到 Github 上,地址分别是[flutter_weather_bloc](https://github.com/tamarous/flutter_weather_bloc)
和[flutter_weather_redux](https://github.com/tamarous/flutter_weather_redux)
。