视图与逻辑分离之道序篇-使用MVVM模式管理状态(GetStat
了解 GetState
❓ 为什么做GetState
Flutter 状态管理方案百花齐放, 从 ScopeModel 到 Provide、MobX, 再到 BLoC、Redux、Provider. 特别是BLoC和Provider, 已经有了大量的用户,但是我在实际使用的时候,发现了这样几个问题:
- 使用不便,需要手动编写大量的样板代码,状态都需要手动注册
- 业务逻辑与UI表现逻辑, 甚至直接与UI耦合。
- 面对大型项目无法清晰的为各层次划清界限, 单元测试代码编写繁琐。
面对这些问题,GetState应运而生
- 自动注册状态: 解放双手, 保护头发
- 极致的速度: GetState提供时间复杂度为O(1)的访问性能, 暴打一众O(N)的状态管理方案
- 便于单元测试: 业务逻辑与UI代码解耦, 妈妈再也不用担心我的单元测试了, 保护头发*2
- 状态时光机: 使用Recorder, 在过去与现在之间穿梭
- 使用灵活: 既支持Provider使用的mutable状态,也支持BLoC,Redux使用的immutable状态
- 强大的兼容性: 如果你已经使用了Provider, Redux, BLoC等状态管理方案, 那么切换到GetState, 你并不需要移除已有的状态管理代码, GetState可以与现有的状态管理方案共存.
</br> </br>
</br> </br>
GetState : 致力于解决Flutter应用UI与业务逻辑解耦问题的MVVM状态管理方案
</br> </br>
进入正题
🛸 先放上 Pub 以及 项目地址
欢迎Star, PR, issue 😘
前三个Demo分别介绍ViewModel,View和Model,心急的可以直接跳过, 或者配合教程3阅读Demo3
以下是教程中的Demo源码
-
🛴 了解原理 ViewModel的作用: ViewModel登场.dart
-
🚲 包装一个View: 带上View.dart
-
🛵 自定义 Model: M, V, VM一家要整整齐齐.dart
-
🚗 司机上路: 半自动注册状态与跨页状态修改.dart
</br>
🛴了解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.
</br></br></br>
</br>
🚲 包装一个View (Demo1)
直接将ViewModel和GetIt实例裸露在外一点也不优雅, 如果封装为View使用起来可就方便多了
0-先确保配置了yaml
yaml内容 跟Demo0一样
1-再编写ViewModel
这里直接使用Demo0中的ViewModel
2-编写View类(MyCounterView)
View类负责UI绘制, 控制UI的逻辑应当尽量放在View里面, 业务逻辑可以放在ViewModel中,
一个ViewModel经常会对应多个View, 根据迪米特原则, 各个View应当在其内部处理好UI绘制逻辑.
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
</br></br></br>
</br>
🛵 自定义 Model (Demo2)
在实际应用中, Model肯定不会是一个基本类型, 否则也就失去使用状态管理的意义了
✨ 建议自己动手的时候也按照本文中的步骤操作
</br>
0-先确保配置了yaml
dependencies:
flutter:
sdk: flutter
## 1. 引入get_state
get_state: ^3.3.0
## 2- 可以通过引入equatable,省去手动覆写==和hashCode
equatable: ^1.1.1
</br>
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;
}
</br>
2-编写ViewModel(CounterVm)
这里沿用Demo0中的代码
</br>
3-编写View(MyCounterView)
这里沿用Demo1中的View
</br>
4-再将View放入Widget树
仍然沿用Demo1中的代码
</br>
5-最后不要忘记注册依赖(自动注册就不用考虑这一步了)
还是用Demo0中的依赖注册方式
</br>
GetState基础使用教程至此结束, 是不是十分简单呢? 😎
</br></br>
</br>
🚗 半自动注册状态与跨页状态修改 (Demo3)
😀 emmm, 不用多说, 肯定有全自动注册的方法了,
不过由于篇幅有限, 全自动注册的方法请参考 这里, 这里不再做详细说明(不建议新手使用)
</br>
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
</br>
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];
}
</br>
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, '新的值'));
}
}
</br>
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(),
),
);
}
</br>
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,
),
</br>
2-1页面B-创建Model
页面1的MVVM一家已经创建完毕了, 页面2只是为了演示跨页状态的修改, 所以就随便写一下
// 你没看错, 页面2不定义Model了, 直接用int类型吧
</br>
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(),
),
);
}
</br>
3-1初始化Injectable
将下面的函数直接写在main.dart文件里面, 当然,另外创建一个新dart文件也可以, 问题不大.
函数, 一定要放在类的外面, 放在类里面的叫方法.
同样不要放了添加注解"@injectableInit".
建议直接复制下面的代码到自己项目里
写好之后, IDE会提示"找不到$initGetIt"函数,
不要着急, 这个函数还没有自动生成呢
// 添加注解
@injectableInit
Future<void> configDi() async {
$initGetIt(g);
}
❗ 注意,这里的 configDi方法返回值是 Future<void>,
但是函数体内没有await.
这是因为当前生成的依赖注入代码都是同步的, 如果用到了@preResolve注解, 则生成的 $initGetIt()是一个异步方法, 必须要加上await,否则会出错
</br>
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文件就可以了.
</br>
4-在main中添加依赖注入
GetIt g = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 5. 添加自动依赖注入
configDi();
runApp(MaterialApp(home: MyApp()));
}
</br></br></br>
</br>
🎇🎇🎇大功告成🎇🎇🎇
</br>
以上Demo就是get_state的一般用法了, 不过除此之外, get_state还有更多技巧等待你的解锁😀
下面几个Demo的依赖于这个文件.dart, 直接复制粘贴是无法运行的, 具体原因是因为没有为自己生成相应的 依赖注入代码
-
🚙 页面级注册: 进入页面时注册状态,退出即销毁.dart
-
🚐
ViewModel异步初始化: 其实我觉得这个功能用处不大.dart
- 🚒 状态时光机 在过去与现在之间反复横跳.dart
</br>
</br>
希望各位多多点赞支持, 更欢迎大家提出意见与建议😀
有时间的话会补上后续教程的😜
后续
- 关于上文中留下的问题
"💡 猜一猜复杂的业务逻辑应该怎么处理", 请参见GetArch介绍
</br>
新增
-
🚙🚙视图级注册: 创建View时注册, dispose时即销毁.dart
-
🚐 自动生成带有copyWith()的Model类: 通过copyWith()修改属性真的不会影响性能.dart
-
🚜将View携带的参数传给ViewModel: 页面传参小帮手.dart
✨✨
未经作者授权, 禁止转载