视图与逻辑分离之道序篇-使用MVVM模式管理状态(GetState)

了解 GetState

❓ 为何作GetState

Flutter 状态管理方案百花齐放, 从 ScopeModel 到 Provide、MobX, 再到 BLoC、Redux、Provider. 特别是BLoC和Provider, 已经有了大量的用户,可是我在实际使用的时候,发现了这样几个问题:git

  • 使用不便,须要手动编写大量的样板代码,状态都须要手动注册
  • 业务逻辑与UI表现逻辑, 甚至直接与UI耦合。
  • 面对大型项目没法清晰的为各层次划清界限, 单元测试代码编写繁琐。

面对这些问题,GetState应运而生github

  • 自动注册状态: 解放双手, 保护头发
  • 极致的速度: GetState提供时间复杂度为O(1)的访问性能, 暴打一众O(N)的状态管理方案
  • 便于单元测试: 业务逻辑与UI代码解耦, 妈妈不再用担忧个人单元测试了, 保护头发*2
  • 状态时光机: 使用Recorder, 在过去与如今之间穿梭

GetState目前仍然有不少不足之处, 但愿你们多多PR, issue 💕markdown

GetState : 致力于解决Flutter应用UI与业务逻辑解耦问题的MVVM状态管理方案app

进入正题

🛸 先放上 Pub 以及 项目地址

欢迎Star, PR, issue 😘less

前三个Demo分别介绍ViewModel,View和Model,心急的能够直接跳过, 或者配合教程3阅读Demo3异步

如下是教程中的Demo源码async


🛴了解GetState原理 - ViewModel的做用 (Demo0)

按照Flutter的惯例, 第一个Demo固然是选择经典的CounterApp了

👻 不推荐本例中的写法, Demo仅供了解GetState原理

0-确保配置yaml配置正确

dependencies:
 flutter:
 sdk: flutter
  ## 引入get_state
 get_state: <这里填写版本号>
复制代码

1-编写viewmodel类-countervm

ViewModel负责简单的业务逻辑和操做视图

💡 猜一猜复杂的业务逻辑应该怎么处理

这里的操做Model的方法(如incrementCounter),至关于BLoC中的Event

ViewModel的泛型即Model的类型, 这里直接使用int类型, 固然也可使用自定义类型, 详见后面"推荐用法"

class CounterVm extends ViewModel<int> {
  // 1.1 在ViewModel的构造中, 提供默认的初始值
  CounterVm() : super(initModel: 0);

  // 1.2 获取Model方法, 这里的model时父类中的属性,其类型用本类泛型指定
  int counter()=> m;

  // 1.3 操做Model方法,
  // 调用 父类中的vmUpdate(M m)方法更新model的值
  void incrementCounter() {
    vmUpdate(m + 1);
  }
}
复制代码

2-在main方法中注册ViewModel(手动注册方式)

😃 既然有"手动注册"方式, 那么确定有自动注册方式了, 详见后面的代码

使用 GetIt g = GetIt.instance; 获取GetIt实例.

实际上直接使用GetIt.instance或GetIt.I效果是同样的,且它们都是单例模式. 这里将其赋值给 g,只是为了便于使用.
固然, 推荐命名为 _g

添加 WidgetsFlutterBinding.ensureInitialized();以防止ViewModel注册失败

关于WidgetsFlutterBinding.ensureInitialized()的做用,这里贴出Flutter源码中的说明
"You only need to call this method if you need the binding to be initialized before calling [runApp]."

使用 GetIt.I.registerSingleton<泛型>(构造方法); 以懒单例的方式注册ViewModel

get_it 还有更多注册方式, 这里暂时只介绍懒单例注册方式

GetIt g = GetIt.instance;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 4.手动注入依赖, 确保View能够获取到ViewModel
  g.registerSingleton<CounterVm>(CounterVm());
  runApp(MyApp());
}
复制代码

3-最后,在UI代码中调用ViewMdoel的方法来操做与获取数据

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('演示:0.极简使用方法'),
          ),
          body: Center(
            child: Text('测试0: ${g<CounterVm>().counter()}'),
          ),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => g<CounterVm>().incrementCounter(),
          ),
        ),
      );
}
复制代码

Demo1到此结束了, 本例仅供了解GetState原理, 实际使用中, 不建议使用这样的写法.标准写法见Demo3
接下来是包装View的Demo.






🚲 包装一个View (Demo1)

直接将ViewModel和GetIt实例裸露在外一点也不优雅, 若是封装为View使用起来可就方便多了

0-先确保配置了yaml

yaml内容 跟Demo0同样

1-再编写ViewModel

这里直接使用Demo0中的ViewModel

2-编写View类(MyCounterView)

View类只负责视图展现, 尽可能将操做视图的代码移动到 ViewModel中

View就是最终展现出来的Widget

class MyCounterView extends View<MyCounterViewModel> {
  @override
  Widget build(BuildContext c, MyCounterViewModel vm) => ListTile(
        title: Text('测试1: ${vm.counter}'),
        trailing: RaisedButton(
          child: Icon(Icons.add),
          onPressed: () => vm.incrementCounter(),
        ),
      );
}
复制代码

3-将View放到Widget树中

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: '演示:1.初级使用方法',
        home: Scaffold(
          appBar: AppBar(),
          body: Column(children: <Widget>[
            // 将视图放入须要的地方
            MyCounterView(),
          ]),
        ),
      );
}
复制代码

3-在main方法中注册依赖

这里仍是沿用 Demo0中的方法

包装View的Demo到此结束, 这样的写法适用于Model十分简单的状况, 但实际上若是Model十分简单, 也就失去使用状态管理的意义了, 图一乐也就图一乐,真图一乐还得看Demo3






🛵 自定义 Model (Demo2)

在实际应用中, Model确定不会是一个基本类型, 不然也就失去使用状态管理的意义了

✨ 建议本身动手的时候也按照本文中的步骤操做


0-先确保配置了yaml

dependencies:
 flutter:
 sdk: flutter
  ## 1. 引入get_state
 get_state: ^3.3.0

  ## 2- 能够经过引入equatable,省去手动覆写==和hashCode
 equatable: ^1.1.1
复制代码

1-编写Model(CounterModel)

创建一个简单的状态, 内部有两个变量 number和str
Model有两种写法, 其实本质上没有区别, 先看看写法1

/// 写法1
class CounterModel {
  final int number;
  final String str;

  CounterModel(this.number, this.str);

  // todo 注意, 这里务必覆写==与hashCode, 不然没法正常刷新
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is CounterModel &&
          runtimeType == other.runtimeType &&
          number == other.number &&
          str == other.str;

  @override
  int get hashCode => number.hashCode ^ str.hashCode;
}
复制代码

✨ 这里推荐写法2, 使用Equatable贯彻"解放双手,保护头发"的理念.

虽然有IDE加持, 覆写==与hashCode方法并通常不费时间.
但若是Model中的字段不少,频繁修改字段的同时, 还要修改 ==与hashCode方法, 太过麻烦.

/// 写法2: 使用 Equatable
class CounterModel2 extends Equatable {
  final int number;
  final String str;

  CounterModel2(this.number, this.str);

  // todo 这里须要将全部的属性值都放入 props中
  @override
  List<Object> get props => [number, str];
  
  // ✨ 小技巧, 添加下面这行代码,连toString都不用动手了
  @override
  final stringify = true;
}
复制代码

2-编写ViewModel(CounterVm)

这里沿用Demo0中的代码


3-编写View(MyCounterView)

这里沿用Demo1中的View


4-再将View放入Widget树

仍然沿用Demo1中的代码


5-最后不要忘记注册依赖(自动注册就不用考虑这一步了)

仍是用Demo0中的依赖注册方式


GetState基础使用教程至此结束, 是否是十分简单呢? 😎





🚗 半自动注册状态与跨页状态修改 (Demo3)

😀 emmm, 不用多说, 确定有全自动注册的方法了, 不过因为篇幅有限, 全自动注册的方法请参考 这里, 这里再也不作详细说明(不建议新手使用)


0-先确保配置了yaml

❗ 这里的yaml与以前的相差较大, 注意观察

dependencies:
 flutter:
 sdk: flutter
  ## 1. 引入get_state
 get_state: ^3.3.0

  ## 2- 能够经过引入equatable,省去手动覆写==和hashCode
 equatable: ^1.2.0
  
  ## 3- 经过injectable省去手动注册步骤
 injectable: ^0.4.0+1

dev_dependencies:
 flutter_test:
 sdk: flutter
  ## 4- injectable须要额外添加下面两个依赖
 build_runner: ^1.10.0
  ## 5- 这个一样重要
 injectable_generator: ^0.4.1
复制代码

1-1页面A-建立Model(CounterModel2)

本Demo将会建立两个Page, 先看第一个页面.
Model内容与上一个Demo中的CounterModel基本一致

class CounterModel2 extends Equatable {
  final int number;
  final String str;

  CounterModel2(this.number, this.str);

  // 1. 这里须要将全部的属性值都放入 props中
  @override
  List<Object> get props => [number, str];
}
复制代码

1-2页面A-建立ViewModel(MyCounterViewModel)

👻 这里要注意, 必定要添加"@lazySingleton"注解, 这就是"半自动"的一部分, 千万不要省略

不是光加上注解的完事了, "半自动"还有另外一半操做呢😜

@lazySingleton
class MyCounterViewModel extends ViewModel<CounterModel2> {
  MyCounterViewModel() : super(initModel: CounterModel2(3, '- -'));

  int get counter => m.number;

  void incrementCounter() {
    vmUpdate(CounterModel2(m.number + 1, '新的值'));
  }
}
复制代码

1-3页面A-建立View(MyCounterView)

class MyCounterView extends View<MyCounterViewModel> {
  @override
  Widget build(BuildContext c, MyCounterViewModel vm) => ListTile(
        leading: Text('测试3: ${vm.counter}'),
        title: Text('${vm.m.str}'),
        trailing: RaisedButton(
          child: Icon(Icons.add),
          onPressed: () => vm.incrementCounter(),
        ),
      );
}
复制代码

1-4页面A-将View放到Page中

这里的MapApp 跟前面的不太同样, 不要太在乎这些细节, 问题不大

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text('演示:3.标准使用方法'),
        ),
        body: Column(children: <Widget>[
          // View 1
          MyCounterView(),
          RaisedButton(
            child: Text('跳转到新页面'),
            onPressed: () => Navigator.of(context).push(MaterialPageRoute(
              builder: (c) => Page2(),
            )),
          ),
          RaisedButton(
            child: Text('点击更改另外一个页面的值'),
            onPressed: () => g<Pg2Vm>().add,
          ),
        ]),
      );
}
复制代码

看这里, "跨页修改状态"就是这么简单粗暴 😎

RaisedButton(
  child: Text('点击更改另外一个页面的值'),
  onPressed: () => g<Pg2Vm>().add,
),
复制代码

2-1页面B-建立Model

页面1的MVVM一家已经建立完毕了, 页面2只是为了演示跨页状态的修改, 因此就随便写一下

// 你没看错, 页面2不定义Model了, 直接用int类型吧
复制代码

2-2页面B-建立ViewModel(Pg2Vm)

跟上面同样, 一样不要忘记加上"@lazySingleton"

@lazySingleton
class Pg2Vm extends ViewModel<int> {
  Pg2Vm() : super(initModel: 3);

  String get strVal => "$m";

  get add => vmUpdate(m + 1);
}
复制代码

2-3页面B-建立View(FooView)

再建立一个简单的View, 包装如下ViewModel

class FooView extends View<Pg2Vm> {
  @override
  Widget build(BuildContext c, Pg2Vm vm) => RaisedButton(
        child: Text('${vm.strVal}'),
        onPressed: () => vm.add,
      );
}
复制代码

2-4页面B-将View放入Page中

class Page2 extends StatelessWidget{
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(),
        body: Center(
          child: FooView(),
        ),
      );
}
复制代码

3-1初始化Injectable

将下面的函数直接写在main.dart文件里面, 固然,另外建立一个新dart文件也能够, 问题不大.

函数, 必定要放在类的外面, 放在类里面的叫方法.

一样不要放了添加注解"@injectableInit".
建议直接复制下面的代码到本身项目里

写好以后, IDE会提示"找不到$initGetIt"函数, 不要着急, 这个函数尚未自动生成呢

// 添加注解
@injectableInit
Future<void> configDi() async {
  $initGetIt(g);
}
复制代码

❗ 注意,这里的 configDi方法返回值是 Future, 可是函数体内没有await.
这是由于当前生成的依赖注入代码都是同步的, 若是用到了@preResolve注解, 则生成的 $initGetIt()是一个异步方法, 必需要加上await,不然会出错


3-2自动生成注入代码

打开Terminal(或者用CMD进入项目的lib同级路径), 输入

flutter pub run build_runner build --delete-conflicting-outputs
复制代码

若是但愿build_runner在后台持续自动生成代码,则输入

flutter pub run build_runner watch --delete-conflicting-outputs
复制代码

这里的"--delete-conflicting-outputs"表示清除已经生成过的代码, 若是你以前已经生成过代码, 而第二次生成又不想从新开始, 则能够不加这个参数

若是生成失败, 注意查看错误代码, 通常状况下加上"--delete-conflicting-outputs"就能解决问题

待代码生成完毕后, 在本来报错的代码处import新生成的 xxx.iconfig.dart文件就能够了.


4-在main中添加依赖注入

GetIt g = GetIt.instance;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 5. 添加自动依赖注入
  configDi();
  runApp(MaterialApp(home: MyApp()));
}
复制代码






🎇🎇🎇大功告成🎇🎇🎇


以上Demo就是get_state的通常用法了, 不过除此以外, get_state还有更多技巧等待你的解锁😀

下面几个Demo的依赖于这个文件.dart, 直接复制粘贴是没法运行的, 具体缘由是由于没有为本身生成相应的 依赖注入代码



但愿各位多多点赞支持, 更欢迎你们提出意见与建议😀

有时间的话会补上后3个教程的😜

✨✨

后续

  • 关于上文中留下的问题

"💡 猜一猜复杂的业务逻辑应该怎么处理", 请参见GetArch介绍

  • GetState 新版本已支持View级ViewModel自动注册, 相比页面级注册, 使用更方便,详见 项目 中的example
相关文章
相关标签/搜索