Flutter 状态管理 Provider的使用和实现MVVM
在使用Flutter开发一款APP之前,通常我们需要考虑如何设计工程的状态管理架构;选择一种结构清晰、易于维护的方式对于APP开发来说就显得十分有必要。
本文我们就来介绍如何使用官方推荐的Provider
来实现MVVM架构的状态管理。
什么是Flutter中的MVVM?
MVVM拆解来说就是三个部分:
- Model
数据模型。通常来说,Model中保存了相关业务的数据,比如说用户(User),它其中包含id
、name
、password
。它就是一个Model。 - View
视图。通俗讲就是展示给用户的界面及控件,比如Flutter中参与界面展示的Widget。为什么我们要强调参与界面展示的Widget呢?因为在Flutter中几乎所有的东西都可以理解为Widget。 - ViewModel
负责实现View与Model的交互。这个是最关键的部分,ViewModel将视图和数据模型进行解耦,并且负责他们之间的交互。简单讲就是所有的业务逻辑都由它负责,而不是将业务逻辑和View都糅合在一起。
如果您熟悉安卓开发,也可以参考我之前的文章使用DataBinding实现MVVM,了解一下安卓开发上的MVVM架构。
Flutter中的MVVM模式的几种方式
在不使用任何第三方包的时候,官方也提供了不错的选择,那就是StatefulWidget
,当我们需要改变状态来刷新UI时,只需要调用setState()
方法。
这种方法简单直接,而且也可以理解为一种MVVM模式,只不过View和Model仍然耦合在一起,ViewModel并没有承担起它应有的角色。随着我们的工程变得越来越大时,代码里的setState()
就会变得越来越多,显得非常混乱,并且有时候会忘记调用setState()
,导致浪费很多时间来定位问题。
官方早期也提供的一种状态管理模式叫做BLOC
。这种方式依赖于第三方包rxDart
,以流(Stream)的方式很好地解决了setState()
的问题。但是这种学习难度较大,对Flutter的新手并不友好。后来出现了一种第三方库Provider
,这是一种先进的状态管理和依赖注入的工具,并且易于学习和理解,所以目前官方也推荐首选Provider
。
本文我们也是主要介绍如何使用Provider
来实现MVVM模式。
初始化工程
为了专注讲解如何使用MVVM架构,这里就不从创建工程开始讲了。这里我创建了一个初始工程,从我现有的APP中抽出了一个登录页面,去掉了一些不必要的代码。可以直接点击在这里下载。这个初始工程中包含了以下内容:
- 页面路由设置
- 用户登录页面
- 一个空的首页
- API类用来模拟网络请求
- 模拟发送验证码和登录请求
- 一个简单的用户Model
登录页的界面如图:
加入Provider库
我们引入写此文最新Provider版本的依赖:
1 2 |
dependencies: provider: ^4.1.0 |
由于APP中有可能需要使用到多个Provider
以提供不同的功能,如果我们使用老版本的Provider时可能需要这样写:
1 2 3 4 5 6 7 8 9 10 |
Provider<Something>( create: (_) => Something(), child: Provider<SomethingElse>( create: (_) => SomethingElse(), child: Provider<AnotherThing>( create: (_) => AnotherThing(), child: someWidget, ), ), ), |
好在新版本中我们有了MultiProvider
,可以将多个Provider集合在一起,我们不必再写如此多的层级了。只需这样写,传入provider列表即可:
1 2 3 4 |
MultiProvider( providers: providers, child: someWidget, ) |
创建Provider服务配置列表
我们创建dart文件provider_setup.dart
,然后将APP中所需要的服务配置好。如果有了解服务端Spring Boot的朋友,可以对比其中的Bean的注入理解一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import 'package:flutter_provider_mvvm/api.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; List<SingleChildWidget> providers = [ ...independentServices, ...dependentServices, ]; List<SingleChildWidget> independentServices = [ Provider(create: (_) => Api()), ]; List<SingleChildWidget> dependentServices = [ //这里使用ProxyProvider来定义需要依赖其他Provider的服务 ]; |
这里我们将APP中需要的服务都定义到这里。如果一个Provider依赖另一个Provider,我们可以使用ProxyProvider。这里我们为了保持简单,只定义了网络请求服务Api()
。
将Provider应用于整个APP
由于我们整个APP都需要使用Provider进行状态管理,所以我们需要在main.dart
中将整个APP包裹在MultiProvider中,并且将上面创建的所有Provider列表传入到参数providers中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MultiProvider( providers: providers, child: MaterialApp( title: 'Flutter MVVM', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: RoutePaths.LOGIN, onGenerateRoute: Router.generateRoute, ), ); } } |
使用ViewModel进行状态管理
既然我们在MVVM模式模式下进行开发APP,那么ViewModel是必不可少的。也就是当状态属性变化时,我们需要UI(也就是View层)进行相应的更改。
Provider中有ChangeNotifierProvider
可以帮助我们监听是否状态发生了变化,它的child参数是一个Consumer
可以帮我们来消费状态的变化。通俗来讲就是在这里调用Widget的build方法来进行UI刷新。
那在哪里去触发状态变化的通知呢?答案就是使用ChangeNotifier
,当调用其中的notifyListeners()
方法时,就可以通知监听它的ChangeNotifierProvider
进行刷新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@override Widget build(BuildContext context) { return ChangeNotifierProvider<T>( child: Consumer<T>( //Widget的builder方法与child builder: widget.builder, child: widget.child, ), create: (BuildContext context) { //这里是我们的ViewModel,一个ChangeNotifier return model; }, ); } |
现在我们的生产者消费者都有了,可以完善我们的MVVM模式了。
我们先创建一个ViewModel的基类,继承自ChangeNotifier
,这样我们可以将一些公共的属性加入到里面,比如api,页面是否正在加载等:
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 |
class BaseModel extends ChangeNotifier { Api api; bool disposed = false; BaseModel({@required Api api}) : api = api; ViewState _state = ViewState.Idle; ViewState get state => _state; void setState(ViewState viewState) { _state = viewState; notifyListeners(); } @override void dispose() { super.dispose(); disposed = true; } @override void notifyListeners() { if (!disposed) { super.notifyListeners(); } } } |
这里的ViewState
是一个页面状态的枚举,其中标识了页面是否处于加载或者空闲的状态,UI也可以根据这个状态来对应展示。
1 |
enum ViewState { Idle, Busy } |
ChangeNotifier
中提供了销毁方法dispose(),我们可以在这个方法里标记页面是否已经被销毁。如果被销毁的话,我们不再通知页面进行刷新。
支持ChangeNotifierProvider的Widget基类
我们不可能在每一个Widget都使用ChangeNotifierProvider
来包裹一下,所以这里我们需要一个Widget基类:
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 |
class BaseView<T extends BaseModel> extends StatefulWidget { final Widget Function(BuildContext context, T model, Widget child) builder; final T model; final Widget child; final Function(T) onModelReady; BaseView({Key key, this.model, this.builder, this.child, this.onModelReady}) : super(key: key); @override _BaseViewState<T> createState() => _BaseViewState<T>(); } class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> { T model; @override void initState() { model = widget.model; if (widget.onModelReady != null) { widget.onModelReady(model); } super.initState(); } @override Widget build(BuildContext context) { return ChangeNotifierProvider<T>( child: Consumer<T>( builder: widget.builder, child: widget.child, ), create: (BuildContext context) { return model; }, ); } } |
为什么这里我们需要使用StatefulWidget呢?因为我们需要在initState()在所有的子类中给出初始化的机会。
在所有需要应用MVVM模式的Widget都可以继承这个基类,传入ChangeNotifierProvider
所需要的参数,其中包括viewModel,builder,child,还有初始化时的回调方法onModelReady()。
基础工作做完,这样我们可以在我们的APP中应用MVVM模式了。
LoginViewModel
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 |
class LoginViewModel extends BaseModel { LoginViewModel({@required Api api}) : super(api: api); Timer _timer; int _countdownTime = 0; Future<void> sendSms(String mobile) async { await api.sensSms(mobile); } Future<bool> login(String mobile, String sms) async { return await api.login(mobile, sms) != null; } void startCountdown() { _countdownTime = 60; if (_timer == null) { _timer = Timer.periodic(Duration(seconds: 1), (timer) { countdownTime--; if (countdownTime == 0) { cancelTimer(); } }); } } void cancelTimer() { if (_timer != null) { _timer.cancel(); _timer = null; } } @override void dispose() { cancelTimer(); super.dispose(); } int get countdownTime => _countdownTime; set countdownTime(int value) { _countdownTime = value; notifyListeners(); } } |
修改登录页面
下面是我们修改后的登录页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class _LoginViewState extends State<LoginView> { final mobileTextController = TextEditingController(); final smsTextController = TextEditingController(); @override Widget build(BuildContext context) { return BaseView<LoginViewModel>( model: LoginViewModel(api: Provider.of(context)), onModelReady: (model) {}, builder: (context, model, child) { return Scaffold( backgroundColor: Color(0xFFF5F5F5), appBar: AppBar( title: Text('登录/注册'), ), body: Builder( builder: (context) => _buildLoginContent(context, model), )); }, ); } ... |
由于我们把应用Provider的细节封装成了一个Widget:BaseView
,所以无论应用到哪个页面我们都可以很方便的使用,即使将现在的APP重构为使用Provider的MVVM架构,也不需要很多工作量。
下面我们来看这个页面中比较关键的几个部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
GestureDetector( onTap: () async { if (model.countdownTime == 0) { await model.sendSms(mobileTextController.text); model.startCountdown(); } }, child: Text( model.countdownTime > 0 ? '${model.countdownTime}秒后重新发送' : '请输入短信验证码', style: TextStyle( fontSize: 14, color: model.countdownTime > 0 ? Color(0xFFa5a5a5) : Color(0xFF191919), ), ), ) |
这里是发送短信验证码的按钮。点击文字时请求API,并开始倒计时,每隔1秒model中的countdownTime会自减1。因为我们在set方法中调用了notifyListeners()
,所以它的值改变时,UI也会相应地进行刷新,无须其他操作,也无须在UI中做setState(),或者逻辑判断。UI唯一需要关心的就是model中的属性。
我们将所有的业务逻辑都放在model里面后会使页面清晰而简单。
再比如我们点击登录时的操作,登录成功后跳转到APP的首页:
1 2 3 4 5 6 |
onPressed: () async { if (await model.login( mobileTextController.text, smsTextController.text)) { Navigator.of(context).pushNamed(RoutePaths.HOME); } } |
将Provider应用到APP首页
从上面登录页面的示例中,我们可以看到如何使用Provider实现MVVM模式。
下面我们来看登陆成功后跳转到的APP首页。
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 |
class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { return BaseView<HomeViewModel>( onModelReady: (model) async { model.loadData(); }, model: HomeViewModel(api: Provider.of(context)), builder: (context, model, child) => Scaffold( backgroundColor: Color(0xFFF5F5F5), appBar: AppBar( title: Text('首页'), ), body: _buildBody(context, model), ), ); } Widget _buildBody(BuildContext context, HomeViewModel model) { return Container( child: model.state == ViewState.Busy ? Center( child: CircularProgressIndicator(), ) : Center( child: Text('APP首页'), )); } } |
进入到首页后,在onModelReady
的回调方法中我们首先请求Api数据,请求数据时我们会设置页面的状态,标记为繁忙;请求完成时,标记为空闲。UI的展示也根据这个状态展示加载框还是首页内容。
下面是HomeViewModel
中网络请求的部分:
1 2 3 4 5 6 7 |
Future<void> loadData() async { print('加载首页数据...'); setState(ViewState.Busy); await Future.delayed(Duration(seconds: 2)); setState(ViewState.Idle); print('加载首页数据完成'); } |
复杂业务逻辑的细粒化状态管理
对于大部分的业务场景来说,一个页面对应ViewModel。这样做是没问题的,因为许多页面并没有那么复杂。
但是对于类似淘宝、京东首页这种量级的APP来说,整个页面对应一个ViewModel显然是不行的,否则View和ViewModel中的代码会非常庞大,难以维护;并且我们希望某一个功能模块的数据变化只进行局部刷新,而不是整个页面刷新。
那么此时该使用什么办法来解决呢?
那就是将不同的功能分别写在不同的BaseView和ViewModel中,然后再组合使用他们。
因为BaseView也是一个Widget,根据我们的架构,它对应一个ViewModel。这种拆分组合不同功能模块的方式非常容易理解,也比较容易实现;更重要的一点它非常有利于代码重用和扩展,其他同事阅读代码也比较清晰。
最后记住一点:如非必要不要化简为繁,过度封装。
我也已经用这种MVVM架构模式完成了数个基于Flutter的APP,也已经发布到了App Store和安卓市场。这种基于Provider的MVVM架构模式极大地提升了Flutter的开发体验,并且也易于维护和扩展。
毫无疑问Flutter会是将来的发展趋势,后续我也会继续探索分享Flutter的开发心得和其他的互联网技术,欢迎关注我刚开通的公众号”程序员磊哥“,谢谢~
这个工程的完整的代码我也上传到了Github上面,可以点击这里下载查看。