Flutter 状态管理 Provider的使用和实现MVVM

来源:www.examplecode.cn 时间:2020-09-15 17:04

在使用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上面,可以点击这里下载查看