Flutter 状态管理方案百花齐放, 从 ScopeModel 到 Provide、MobX, 再到 BLoC、Redux、Provider. 特别是BLoC和Provider, 已经有了大量的用户,可是我在实际使用的时候,发现了这样几个问题:git
面对这些问题,GetState应运而生github
GetState目前仍然有不少不足之处, 但愿你们多多PR, issue 💕markdown
GetState : 致力于解决Flutter应用UI与业务逻辑解耦问题的MVVM状态管理方案app
欢迎Star, PR, issue 😘less
前三个Demo分别介绍ViewModel,View和Model,心急的能够直接跳过, 或者配合教程3阅读Demo3异步
如下是教程中的Demo源码async
🛴 了解原理 ViewModel的做用: ViewModel登场.dartide
🚲 包装一个View: 带上View.dart函数
🛵 自定义 Model: M, V, VM一家要整整齐齐.dartpost
🚗 司机上路: 半自动注册状态与跨页状态修改.dart
按照Flutter的惯例, 第一个Demo固然是选择经典的CounterApp了
👻 不推荐本例中的写法, Demo仅供了解GetState原理
dependencies:
flutter:
sdk: flutter
## 引入get_state
get_state: <这里填写版本号>
复制代码
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);
}
}
复制代码
😃 既然有"手动注册"方式, 那么确定有自动注册方式了, 详见后面的代码
使用 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());
}
复制代码
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.
直接将ViewModel和GetIt实例裸露在外一点也不优雅, 若是封装为View使用起来可就方便多了
yaml内容 跟Demo0同样
这里直接使用Demo0中的ViewModel
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(),
),
);
}
复制代码
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: '演示:1.初级使用方法',
home: Scaffold(
appBar: AppBar(),
body: Column(children: <Widget>[
// 将视图放入须要的地方
MyCounterView(),
]),
),
);
}
复制代码
这里仍是沿用 Demo0中的方法
包装View的Demo到此结束, 这样的写法适用于Model十分简单的状况, 但实际上若是Model十分简单, 也就失去使用状态管理的意义了, 图一乐也就图一乐,真图一乐还得看Demo3
在实际应用中, Model确定不会是一个基本类型, 不然也就失去使用状态管理的意义了
✨ 建议本身动手的时候也按照本文中的步骤操做
dependencies:
flutter:
sdk: flutter
## 1. 引入get_state
get_state: ^3.3.0
## 2- 能够经过引入equatable,省去手动覆写==和hashCode
equatable: ^1.1.1
复制代码
创建一个简单的状态, 内部有两个变量 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;
}
复制代码
这里沿用Demo0中的代码
这里沿用Demo1中的View
仍然沿用Demo1中的代码
仍是用Demo0中的依赖注册方式
GetState基础使用教程至此结束, 是否是十分简单呢? 😎
😀 emmm, 不用多说, 确定有全自动注册的方法了, 不过因为篇幅有限, 全自动注册的方法请参考 这里, 这里再也不作详细说明(不建议新手使用)
❗ 这里的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
复制代码
本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];
}
复制代码
👻 这里要注意, 必定要添加"@lazySingleton"注解, 这就是"半自动"的一部分, 千万不要省略
不是光加上注解的完事了, "半自动"还有另外一半操做呢😜
@lazySingleton
class MyCounterViewModel extends ViewModel<CounterModel2> {
MyCounterViewModel() : super(initModel: CounterModel2(3, '- -'));
int get counter => m.number;
void incrementCounter() {
vmUpdate(CounterModel2(m.number + 1, '新的值'));
}
}
复制代码
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(),
),
);
}
复制代码
这里的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,
),
复制代码
页面1的MVVM一家已经建立完毕了, 页面2只是为了演示跨页状态的修改, 因此就随便写一下
// 你没看错, 页面2不定义Model了, 直接用int类型吧
复制代码
跟上面同样, 一样不要忘记加上"@lazySingleton"
@lazySingleton
class Pg2Vm extends ViewModel<int> {
Pg2Vm() : super(initModel: 3);
String get strVal => "$m";
get add => vmUpdate(m + 1);
}
复制代码
再建立一个简单的View, 包装如下ViewModel
class FooView extends View<Pg2Vm> {
@override
Widget build(BuildContext c, Pg2Vm vm) => RaisedButton(
child: Text('${vm.strVal}'),
onPressed: () => vm.add,
);
}
复制代码
class Page2 extends StatelessWidget{
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(),
body: Center(
child: FooView(),
),
);
}
复制代码
将下面的函数直接写在main.dart文件里面, 固然,另外建立一个新dart文件也能够, 问题不大.
函数, 必定要放在类的外面, 放在类里面的叫方法.
一样不要放了添加注解"@injectableInit".
建议直接复制下面的代码到本身项目里
写好以后, IDE会提示"找不到$initGetIt"函数, 不要着急, 这个函数尚未自动生成呢
// 添加注解
@injectableInit
Future<void> configDi() async {
$initGetIt(g);
}
复制代码
❗ 注意,这里的 configDi方法返回值是 Future, 可是函数体内没有await.
这是由于当前生成的依赖注入代码都是同步的, 若是用到了@preResolve注解, 则生成的 $initGetIt()是一个异步方法, 必需要加上await,不然会出错
打开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文件就能够了.
GetIt g = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 5. 添加自动依赖注入
configDi();
runApp(MaterialApp(home: MyApp()));
}
复制代码
以上Demo就是get_state的通常用法了, 不过除此以外, get_state还有更多技巧等待你的解锁😀
下面几个Demo的依赖于这个文件.dart, 直接复制粘贴是没法运行的, 具体缘由是由于没有为本身生成相应的 依赖注入代码
🚙 页面级注册: 进入页面时注册状态,退出即销毁.dart
🚐 ViewModel异步初始化: 其实我以为这个功能用处不大.dart
🚒 状态时光机 在过去与如今之间反复横跳.dart
有时间的话会补上后3个教程的😜
"💡 猜一猜复杂的业务逻辑应该怎么处理", 请参见GetArch介绍