Flutter使用Provider实现MVVM状态管理架构

在使用Flutter开发一款APP之前,通常我们需要考虑如何设计工程的状态管理架构;选择一种结构清晰、易于维护的方式对于APP开发来说就显得十分有必要。

本文我们就来介绍如何使用官方推荐的Provider来实现MVVM架构的状态管理。

什么是Flutter中的MVVM?

MVVM拆解来说就是三个部分:

  • Model
    数据模型。通常来说,Model中保存了相关业务的数据,比如说用户(User),它其中包含idnamepassword。它就是一个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

登录页的界面如图:

-c300

加入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);
}
}

-c300

将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('加载首页数据完成');
}

-c300

-c300

复杂业务逻辑的细粒化状态管理

对于大部分的业务场景来说,一个页面对应ViewModel。这样做是没问题的,因为许多页面并没有那么复杂。

但是对于类似淘宝、京东首页这种量级的APP来说,整个页面对应一个ViewModel显然是不行的,否则View和ViewModel中的代码会非常庞大,难以维护;并且我们希望某一个功能模块的数据变化只进行局部刷新,而不是整个页面刷新。

那么此时该使用什么办法来解决呢?

那就是将不同的功能分别写在不同的BaseView和ViewModel中,然后再组合使用他们。

因为BaseView也是一个Widget,根据我们的架构,它对应一个ViewModel。这种拆分组合不同功能模块的方式非常容易理解,也比较容易实现;更重要的一点它非常有利于代码重用和扩展,其他同事阅读代码也比较清晰。

最后记住一点:如非必要不要化简为繁,过度封装。

我也已经用这种MVVM架构模式完成了数个基于Flutter的APP,也已经发布到了App Store和安卓市场。这种基于Provider的MVVM架构模式极大地提升了Flutter的开发体验,并且也易于维护和扩展。

毫无疑问Flutter会是将来的发展趋势,后续我也会继续探索分享Flutter的开发心得和其他的互联网技术,欢迎关注我刚开通的公众号”程序员磊哥“,谢谢~

这个工程的完整的代码我也上传到了Github上面,可以点击这里下载查看

程序员磊哥 wechat
扫描微信二维码,关注磊哥的公众号