美文网首页跨平台
Flutter了解之入门篇9-2(状态管理库)

Flutter了解之入门篇9-2(状态管理库)

作者: 平安喜乐698 | 来源:发表于2022-09-24 23:23 被阅读0次
  1. MobX库
  2. GetX(流行度超Provider)

1. MobX库

基于观察者模式和响应式模式的状态管理库,将响应式数据和UI进行绑定(绑定是完全自动的)。使开发者只需要关注UI需要消费哪些响应式数据,而无需关注如何保持二者同步。

添加flutter_mobx依赖库后,只需要使用Observer的builder属性包裹依赖状态数据的子组件,状态改变后会自动刷新UI。
Observer(
  builder: (_) => Text(
    '${_counter.value}',
    style: const TextStyle(fontSize: 20),
  ),
),

从源码解析 MobX 响应式刷新机制

Observer类的关系图
响应式无感知刷新的具体实现:
  1. 控制渲染的Element是StatelessObserverElement,该类在mount阶段通过createReaction注册了reaction。
  2. StatelessObserverElement在build方法中将reaction和observable进行绑定。
  3. 在Observer中读取状态对象属性时,会调用到其get方法,该方法会将状态对象属性与对应的Observer组件进行绑定。
  4. 当状态对象的属性被set更改时,会调度到该属性绑定的reaction,执行_onInvalidate方法来进行刷新。

=========================
StatelessObserverWidget定义如下:
// StatelessObserverWidget继承自StatelessWidget,混入了ObserverWidgetMixin
abstract class StatelessObserverWidget extends StatelessWidget
    with ObserverWidgetMixin {  
  /// Initializes [key], [context] and [name] for subclasses.
  const StatelessObserverWidget(
      {Key? key, ReactiveContext? context, String? name})
      : _name = name,
        _context = context,
        super(key: key);
  final String? _name;
  final ReactiveContext? _context;
  @override
  String getName() => _name ?? '$this';
  @override
  ReactiveContext getContext() => _context ?? super.getContext();
  @override
  // 返回的是一个StatelessObserverElement对象(用于控制Element的刷新)。
  StatelessObserverElement createElement() => StatelessObserverElement(this);
}
=========================
ObserverWidgetMixin定义如下:
mixin ObserverWidgetMixin on Widget {
  String getName();
  ReactiveContext getContext() => mainContext;
  @visibleForTesting
  // createReaction方法在ObserverElementMixin中调用
  // 创建reaction,状态改变后会调用reaction
  Reaction createReaction(
    Function() onInvalidate, {
    Function(Object, Reaction)? onError,
  }) =>
      ReactionImpl(
        getContext(),
        onInvalidate,
        name: getName(),
        onError: onError,
      );
  void log(String msg) {
    debugPrint(msg);
  }
}
=========================
StatelessObserverElement继承自StatelessElement仅仅是混入了ObserverElementMixin,
ObserverElementMixin定义如下:
mixin ObserverElementMixin on ComponentElement {
  ReactionImpl get reaction => _reaction;
  late ReactionImpl _reaction;
  ObserverWidgetMixin get _widget => widget as ObserverWidgetMixin;
  @override
  // 重载mount方法,调用createReaction创建reaction,响应方法为invalidate,而 invalidate 方法实际上就是markNeedsBuild方法
  void mount(Element? parent, dynamic newSlot) {
    _reaction = _widget.createReaction(invalidate, onError: (e, _) {
      FlutterError.reportError(FlutterErrorDetails(
        library: 'flutter_mobx',
        exception: e,
        stack: e is Error ? e.stackTrace : null,
      ));
    }) as ReactionImpl;
    super.mount(parent, newSlot);
  }
  // 状态数据改变时通过reaction来调用markNeedsBuild(标记为需要重建)通知Element刷新。
  void invalidate() => markNeedsBuild();
  // 调用了reaction的track方法。会将observer对象和其依赖(Observer的builder返回的widget)进行绑定
  @override
  Widget build() {
    late Widget built;
    reaction.track(() {
      built = super.build();
    });
    if (!reaction.hasObservables) {
      _widget.log(
        'No observables detected in the build method of ${reaction.name}',
      );
    }
    return built;
  }
  @override
  void unmount() {
    reaction.dispose();
    super.unmount();
  }
}

====================
再看MobX(带有@observable)自动生成的代码:
final _$praiseCountAtom = Atom(name: 'ShareStoreBase.praiseCount');
@override
int get praiseCount {
  // reportRead方法会调用_reportObserved方法,将之前Observer绑定的依赖和对应的状态对象属性关联起来。
  _$praiseCountAtom.reportRead(); 
  return super.praiseCount;
}
// set方法其实就是改变了状态对象的属性,这里调用了Atom类的reportWrite方法。会触发注释中的reaction调度方法
/**
void schedule() {
  if (_isScheduled) {
    return;
  }
  _isScheduled = true;
  _context
    ..addPendingReaction(this)
    ..runReactions();
}
这个调度方法最终会执行reaction的_run方法,
*/
@override
set praiseCount(int value) {
  _$praiseCountAtom.reportWrite(value, super.praiseCount, () {
    super.praiseCount = value;
  });
}
=========================
_reportObserved定义如下:
void _reportObserved(Atom atom) {
  final derivation = _state.trackingDerivation;
  if (derivation != null) {
    derivation._newObservables!.add(atom);
    if (!atom._isBeingObserved) {
      atom
        .._isBeingObserved = true
        .._notifyOnBecomeObserved();
    }
  }
}
==========================
// reaction的_run方法
void _run() {  
  if (_isDisposed) {
    return;
  }
  _context.startBatch();
  _isScheduled = false;
  if (_context._shouldCompute(this)) {
    try {
      // _onInvalidate方法正是在ObserverElementMixin中 createReaction的时候传进来的,这个方法会触发 Widget 的 build方法。
      _onInvalidate(); 
    } on Object catch (e, s) {
      // Note: "on Object" accounts for both Error and Exception
      _errorValue = MobXCaughtException(e, stackTrace: s);
      _reportException(_errorValue!);
    }
  }
  _context.endBatch();
}

核心三要素

  1. Observables (响应式状态)
可以是简单对象,也可以是复杂的对象树。

例1  
final counter = Observable(0);

例2
import 'package:mobx/mobx.dart';
class Counter {
  Counter() {
    increment = Action(_increment);
  }
  Action increment;
  void _increment() {
    _value.value++;
  }
  final _value = Observable(0);
  int get value => _value.value;
  set value(int newValue) => _value.value = newValue;
}
改为自动生成(会自动生成.g.dart文件)
  1. 添加依赖库:builder_runner、mobx_codegen
  2. 添加注解:@observable状态属性、@readonly只读、@computed派生状态(依赖其他状态,类似Vue的计算属性)、@action动作方法
  3. 终端执行:flutter packages pub run build_runner build命令
import 'package:mobx/mobx.dart';
part 'counter.g.dart';
class Counter = CounterBase with _$Counter;
abstract class CounterBase with Store {
  @observable
  int value = 0;
  @action
  void increment() {
    value++;
  }
}
  1. Actions (定义了如何改变observables对象)
相比直接更改,actions让更改操作更有语义学的意义(更易于理解和维护)。例如,相比直接使用value++,调用一个increment()动作更易理解。
actions能够分批次处理通知,以确保改变只有在完成之后才会被通知。从而使得观察者是基于action的完成这一原子操作通知的。
actions是可嵌套的,这时只有最顶层的action完成后才会发出通知。

  final counter = Observable(0);
  final increment = Action((){
    counter.value++;
  });
  等价(使用注解自动生成)
  @observable
  int counter = 0;
  @action
  void increment() {
    value++;
  }

对于异步操作,MobX 会自动处理,而无需使用runInAction来包裹。
@observable
String stuff = '';
@observable
loading = false;
@action
Future<void> loadStuff() async {
  loading = true; //This notifies observers
  stuff = await fetchStuff();
  loading = false; //This also notifies observers
}
  1. Reactions(观察者)
一旦跟踪的observable对象改变后会被通知。在reaction中读取observables时就已经自动跟踪该对象了,无需显式绑定。

以下所有方式都会返回一个ReactionDisposer方法,调用该方法可以销毁该reaction。
  1. autorun 方法
    import 'package:mobx/mobx.dart';
    String greeting = Observable('Hello World');
    final dispose = autorun((_){
      print(greeting.value);
    });
    greeting.value = 'Hello MobX';  
    dispose();  
    // 输出: Hello World、Hello MobX
  2. reaction方法
    // 在predicate方法中监测observables对象,然后当predicate返回不同的值时会执行effect方法。且只有predicate中的observables对象会被跟踪。
    ReactionDisposer reaction<T>(T Function(Reaction) predicate, void Function(T) effect)
    和1中不同的部分:final dispose = reaction((_) => greeting.value, (msg) => print(msg));
     // 输出: Hello MobX
  3. when方法
    // 当predicate方法返回true时才执行effect方法。当effect方法运行后,将会自动销毁,可以手动提前销毁。
    ReactionDisposer when(bool Function(Reaction) predicate, void Function() effect)
    和1中不同的部分:final dispose = when((_) => greeting.value == 'Hello MobX', () => print('So so'));
     // 输出: So so
  4. Future 异步方法
    // 和when方法类似,只是返回的结果是一个Future对象——在 predicate 方法返回 true 的时候完成。
    Future<void> asyncWhen(bool Function(Reaction) predicate)  
    //
    final completed = Observable(false);
    void waitForCompletion() async {
      await asyncWhen(() => _completed.value == true);
      print('Completed');
    }

快捷代码片段(VSCode的自定义代码模板功能)

模板代码(对应的json配置)

例(StatefulWidget模版代码对应的json)
"Stateful Widget": {
    "prefix": "statefulW",
    "body": [
        "class ${1:name} extends StatefulWidget {",
        "  ${1:name}({Key? key}) : super(key: key);\n",
        "  @override",
        "  _${1:WidgetName}State createState() => _${1:WidgetName}State();",
        "}\n",
        "class _${1:index}State extends State<${1:index}> {",
        "  @override",
        "  Widget build(BuildContext context) {",
        "    return Container(",
        "       child: ${2:null},",
        "    );",
        "  }",
        "}"
    ],
    "description": "Create a Stateful widget"
},
具体的语法遵循 TextMate规范
  ${1:name} 代表:第1个变量 指定占位符是name
  之后使用${1:xxx}:引用该变量
  多个变量时,后跟$0:表示光标的位置。
例2(MobX模版代码对应的json配置如下)
VSCode|Code->Preferences->User Snippets | New Snippets 输入快捷名。
{
    "Build Mobx Observables" : {
        "prefix": "build mobx",
        "scope": "dart",
        "body": [
            "import 'package:mobx/mobx.dart';\n",
            "part '${TM_FILENAME_BASE}.g.dart';\n",
            "class ${1:name}$0 = $1Base with _$$1;\n",
            "abstract class $1Base with Store {",
            "  @observable",
            "  ${2:type} ${3:property} = ${4:initial};\n",
            "  @action",
            "  void ${5:actionname}() {\n",
            "  }",
            "}",
        ]
    }
}
TM_FILENAME 完整文件名、TM_FILENAME_BASE 不带后缀文件名

MobX本身不支持上级组件状态可以被下级组件共享,需要借助Provider或GetIt来实现状态共享。

示例(借助Provider共享状态)

 class DynamicDetailProvider extends StatelessWidget {
   DynamicDetailProvider({Key? key}) : super(key: key);
   @override
   Widget build(BuildContext context) {
     // 省略其他代码
        child: Provider(
          child: Row(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _PraiseButton(),
              _FavorButton(),
            ],
          ),
          create: (context) => ShareStore(),
        ),
   }
 }
class _FavorButton extends StatelessWidget {
  const _FavorButton({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    print('FavorButton');
    return Container(
      alignment: Alignment.center,
      color: Colors.blue,
      child: TextButton(
        onPressed: () {
          context.read<ShareStore>().increamentFavor();
        },
        child: Observer(
          builder: (context) => Text(
            '收藏 ${context.read<ShareStore>().favorCount}',
            style: TextStyle(color: Colors.white),
          ),
        ),
        style: ButtonStyle(
            minimumSize: MaterialStateProperty.resolveWith(
                (states) => Size((MediaQuery.of(context).size.width / 2), 60))),
      ),
    );
  }
}

示例(借助GetIt共享状态)推荐:更简洁

class DynamicDetailGetIt extends StatelessWidget {
  DynamicDetailGetIt({Key? key}) : super(key: key) {
    GetIt.I.registerSingleton<ShareStore>(ShareStore());
  }
  // 省略 build 方法
}
class _FavorButton extends StatelessWidget {
  const _FavorButton({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    print('FavorButton');
    return Container(
      alignment: Alignment.center,
      color: Colors.blue,
      child: TextButton(
        onPressed: () {
          GetIt.I.get<ShareStore>().increamentFavor();
        },
        child: Observer(
          builder: (context) => Text(
            '收藏 ${GetIt.I.get<ShareStore>().favorCount}',
            style: TextStyle(color: Colors.white),
          ),
        ),
        style: ButtonStyle(
            minimumSize: MaterialStateProperty.resolveWith(
                (states) => Size((MediaQuery.of(context).size.width / 2), 60))),
      ),
    );
  }
}

示例(fl_chart)

见4-1常用三方库篇的fl_chart示例

2. GetX

超轻量、高性能的状态管理库。
涵盖了路由、主题、多语言、弹框、状态管理、依赖注入、网络请求封装等功能。

基本理念
  1. 性能
    GetX 关注性能并最小化资源消耗。
    GetX 不使用 Stream 或 ChangeNotifier。
  2. 生产力
    使用简便,节省开发时间。
  3. 组织性
    GetX 可以将视图、展示逻辑、业务逻辑、依赖注入和导航完全解耦。
    路由之间跳转无需 context,因此导航不会依赖组件树。
    不需要通过 InheritedWidget 的 context 访问控制器或 BLOC 对象,因此可以将展示逻辑和业务逻辑从虚拟的组件层分离。
    不需要像 MultiProvider 那样往组件树中注入 Controller/Model/Bloc 等类对象,因此可以将依赖注入和视图分离。

生态
  1. 每个特性之间是相互独立的,并且只会在使用的时候才启动。例如,如果仅仅是使用状态管理,那么只有状态管理会被编译。而如果只使用路由,那么状态管理的部分就不会编译。
  2. 兼容 Android, iOS, Web, Mac, Linux, Windows多个平台。服务端版本:Get_Server
  3. GET_CLI脚手架;GetX Snippets(VSCode插件)快捷代码片段(getmain获取main.dart代码、getmodel获取包括了fromJson及toJson的Model类代码、getcontroller获取GetxController状态模板代码);

缺陷
  1. 路由需要使用GetMeterialApp包裹,侵入性强。
  2. 路由和弹框内部使用了静态的 context,单元测试没法直接完成,而需要使用 widget testing配合完成。
  3. get_connect 插件集成了 REST API 请求和 GraphQL 客户端。这有点多余,一般的应用不会二者都用。
  4. GetX 的依赖注入还不太成熟,如果依赖对象改变后(比如修改了依赖对象类型,增加了依赖对象),直接热重载会报错,这个时候往往需要 reload 才行。
  5. 源码很多地方缺少注释,导致未来的维护可能会比较麻烦。而官方文档相对也不是很完善,导致使用者需要自己摸索
  6. 源代码组织性比较差
  1. 路由

优势:不依赖于context

// 跳转
Get.to(() => Home());
Get.toNamed('/home');
// 返回上一页
Get.back();
// 替换页面
Get.off(NextScreen());
// 清空导航堆栈中的全部页面
Get.offAll(NextScreen());
// 获取命名路由参数
print(Get.parameters['id']);
print(Get.parameters['name']);
  1. SnackBar
Get.snackbar('SnackBar', '这是GetX的SnackBar');
  1. 对话框
Get.defaultDialog(
  title: '对话框',
  content: Text('对话框内容'),
  onConfirm: () {
    print('Confirm');
    Get.back();
  },
  onCancel: () {
    print('Cancel');
  },
);
  1. 内存缓存
// 缓存内容对象,以便在不同页面共享数据。需要先put再find,否则异常。
Get.put(CacheData(name: '这是缓存数据'));
CacheData cache = Get.find();
  1. 离线存储
get_storage插件(纯Dart编写,不依赖原生)

// GetStorage 是基于内存和文件存储的,当内存容器中有数据时优先从内存读取。
// 同时在构建 GetStorage 对象到时候指定存储的文件名以及存储数据的容器。
GetStorage storage = GetStorage();
storage.write('name', 'hello world');
storage.read('name');
  1. 更改主题
Get.changeTheme(
  Get.isDarkMode ? ThemeData.light() : ThemeData.dark()); // 深色和浅色模式
},
  1. 多语言支持
在 GetMaterialApp 指定字典对象(继承自Translations),使用字符串的时候假设.tr 后缀

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      translations: Messages(),  // 设置translations
      locale: Locale('zh', 'CN'),
      color: Colors.white,
      navigatorKey: Get.key,
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
      ),
      home: GetXDemo(),
    );
  }
}
class GetXDemo extends StatelessWidget {
  // 省略其他代码
  TextButton(
    onPressed: () {
      var locale = Locale('en', 'US');
      Get.updateLocale(locale);  // 切换语言环境
    },
    child: Text('name'.tr),  // 使用
  ),
}
class Messages extends Translations {
  @override
  Map<String, Map<String, String>> get keys => {
        'en_US': {
          'name': 'Dog',
        },
        'zh_CN': {
          'name': '狗',
        }
      };
}
  1. 状态管理

GetxController生命周期

方式1. GetBuilder

1. 状态类(在GetX中称之为 Controller,需要继承GetxController)
  当状态发生改变的时候,调用update方法即可通知依赖状态的组件进行刷新。
  推荐重写onReady方法进行网络请求。
class CounterController extends GetxController {
  int _counter = 0;
  get counter => _counter;
  void increment() {
    _counter++;
    update();
  }
  // 只要状态对象注册一次之后,就可以在任何组件(没有任何关联,不需要拥有共同的父组件)使用CounterController.to访问到
  static CounterController get to => Get.find();
}

2. 界面
  在需要使用状态的地方使用GetBuilder包裹,使用Controller访问状态对象和操作状态方法。
  应该只包裹依赖状态对象的组件。
Widget build(BuildContext context) {
return Scaffold(
  appBar: AppBar(
    title: Text('GetX计数器'),
  ),
  body: Center(
    child: GetBuilder<CounterController>(
      init: CounterController(),
      builder: (_) => Text(   // 这里的参数是controller
        '${CounterController.to.counter}',
        style: TextStyle(
          color: Colors.blue,
          fontSize: 24.0,
        ),
      ),
    ),
  ),
  floatingActionButton: FloatingActionButton(
    child: Icon(Icons.add),
    onPressed: () {
      CounterController.to.increment();
    },
  ),
);

从Provider迁移到GetX修改的代码量不多。

方式2. 响应式状态管理

响应式状态管理相当于实现了状态对象的绑定,只要状态对象发生了改变,依赖状态对象的组件会自动刷新,而不需要手动调用 upade 刷新。
// 为对象创建了一个 Stream,赋予了初始值,然后会通知所有使用该对象的 Widget。一旦这个对象的值发生了改变,就会刷新这些组件。
// 和setState有个区别,GetX 扩展的.obs 用法内部做了是否相等的比较,如果更新操作前后的对象是相等的话,那么不会通知组件刷新,从而提高性能。对于一个Controller拥有多个对象的时候,当这些对象发生改变时,也只会更新那些依赖这些对象的组件,而不是所有依赖 Controller 的组件。
优点:
  1. 无需创建 StreamController;
  2. 无需为每个变量创建 StreamBuilder;
  3. 无需为每个状态创建一个类;
  4. 无需为一个初始值创建一个 get 方法;
  5. 使用GetX响应式编程非常简单,就像使用 setState 一样。


声明状态变量/Rx变量(3种方式)
  1. Rx{Type}
    对于 null safety 版本,必须要提供初始值
  /*
  final name = RxString('');
  final isLogged = RxBool(false);
  final count = RxInt(0);
  final balance = RxDouble(0.0);
  final items = RxList<String>([]);
  final myMap = RxMap<String, int>({});
  */
  2. Rx 泛型
    可以将任何类(包括自定义类型)转为Rx变量。
  /*
  final name = Rx<String>('');
  final isLogged = Rx<Bool>(false);
  final count = Rx<Int>(0);
  final balance = Rx<Double>(0.0);
  final number = Rx<Num>(0);
  final items = Rx<List<String>>([]);
  final myMap = Rx<Map<String, int>>({});
  // 自定义类类型
  final user = Rx<User>();
  */
  3. .obs 扩展
    更简洁
  /*
  final name = 'hello'.obs;
  final isLogged = false.obs;
  final count = 0.obs;
  final balance = 0.0.obs;
  final number = 0.obs;
  final items = <String>[].obs;
  final myMap = <String, int>{}.obs;
  // 自定义类
  final user = User().obs;
  */

使用Rx变量(2种方式)
  1. Obx组件(可以同时监听多个 Controller 的状态对象变化。状态对象 GetxController的生命周期函数不会被调用。Controller 需要在 Obx 之外先初始化)
    Obx(() => Text('${simpleController.name}'),),
  2. GetX组件
    GetX<SimpleReactiveController>(
      builder: (controller) => Text('${simpleController.name}'),
      init: simpleController,
    ),

示例(.obs监测变量)

class SimpleReactiveController extends GetxController {
  final _name = ' hello'.obs;
  set name(value) => this._name.value = value;
  get name => this._name.value;
}
class SimpleReactivePage extends StatelessWidget {
  SimpleReactivePage({Key? key}) : super(key: key);
  final SimpleReactiveController simpleController = SimpleReactiveController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('响应式状态管理'),
      ),
      body: Center(
        child: Obx(
          () => Text('${simpleController.name}'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: () {
          simpleController.name = 'Hi';
        },
      ),
    );
  }
}

定向刷新

当多个组件共用一个状态对象,但更新条件不同时,状态数据改变后刷新指定依赖状态的组件。

看一下update方法的实现:
// ids:要更新的id数组,可以在GetBuilder构建时指定id属性。若指定了ids则之后更新与ids中的id匹配的组件
// condition:只有当这个条件为真的时候才会更新组件。
void update([List<Object>? ids, bool condition = true]) {
  if (!condition) {
    return;
  }
  if (ids == null) {
    refresh();
  } else {
    for (final id in ids) {
      refreshGroup(id);
    }
  }
}
例(在 counter小于10的时候更新id为text的组件):
GetBuilder<Controller>(
  id: 'text'
  init: Controller(),
  builder: (_) => Text(
    '${Get.find<Controller>().counter}', 
  ),
),
update(['text'], counter < 10);

示例(定向刷新)

红绿灯规则如下:
  1. 绿灯亮的时长为20秒,红灯为10秒,黄灯为3秒,计时通过定时器完成,每隔1秒减1。
  2. 三个红灯共用一个计时器,但根据当前亮的灯的状态来定向更新哪个灯的倒计时时间,同时对于不亮的灯不显示倒计时时间(因为共享了倒计时时间,如果显示就会不对)。
  3. 使用一个枚举来确定当前亮哪个灯,亮灯的次序为绿灯->黄灯->红灯->绿灯……

// 一个通用的交通灯组件
class TrafficLed extends StatelessWidget {
  final Color ledColor; // 控制灯的倒计时数字颜色
  final int secondsLeft; // 倒计时时间
  final bool showSeconds; // 是否显示倒计时
  final double ledSize; // 灯的大小
  const TrafficLed({
    Key? key,
    required this.ledColor,
    required this.secondsLeft,
    required this.showSeconds,
    this.ledSize = 60.0,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        alignment: Alignment.center,
        width: ledSize,
        height: ledSize,
        decoration: BoxDecoration(
          color: Colors.black,
          borderRadius: BorderRadius.circular(ledSize / 2),
          boxShadow: [
            BoxShadow(
              color: Color(0xFF505050),
              offset: Offset(1, -1),
              blurRadius: 0.2,
            )
          ],
        ),
        child: Offstage(
          child: Text(
            '$secondsLeft',
            textAlign: TextAlign.center,
            style: TextStyle(
              color: this.ledColor,
              fontSize: 36,
              fontWeight: FontWeight.bold,
            ),
          ),
          offstage: !showSeconds,
        ),
      ),
    );
  }
}

// 状态管理控制器
enum TrafficLight { green, red, yellow }
class TrafficLightController extends GetxController {
  late TrafficLight _currentLight;
  get currentLight => _currentLight;
  int _counter = 0;
  get counter => _counter;
  late Timer _downcountTimer;
  @override
  void onInit() {
    _counter = 20;
    _currentLight = TrafficLight.green;
    super.onInit();
  }
  @override
  void onReady() {
    _downcountTimer = Timer.periodic(Duration(seconds: 1), decreament);
    super.onReady();
  }
  void decreament(Timer timer) {
    _counter--;
    if (_counter == 0) {
      switch (_currentLight) {
        case TrafficLight.green:
          _currentLight = TrafficLight.yellow;
          _counter = 3;
          update(['green', 'yellow']);
          break;
        case TrafficLight.yellow:
          _currentLight = TrafficLight.red;
          _counter = 10;
          update(['red', 'yellow']);
          break;
        case TrafficLight.red:
          _currentLight = TrafficLight.green;
          _counter = 20;
          update(['red', 'green']);
          break;
      }
    } else {
      switch (_currentLight) {
        case TrafficLight.green:
          update(['green']);
          break;
        case TrafficLight.yellow:
          update(['yellow']);
          break;
        case TrafficLight.red:
          update(['red']);
          break;
      }
    }
  }
  @override
  void onClose() {
    _downcountTimer.cancel();
    super.onClose();
  }
}
// 界面
GetBuilder<TrafficLightController>(  
  id: 'green',
  init: lightController,
  builder: (state) => TrafficLed(
    ledColor: (state.currentLight == TrafficLight.green
        ? Colors.green
        : Colors.black),
    secondsLeft: state.counter,
    showSeconds: state.currentLight == TrafficLight.green,
  ),
),
黄灯、红灯逻辑和绿灯类似,注意id区分

使用Worker钩子函数来防抖---避免快速重复点击(减少服务器压力)

为避免Worker反复被注册,应当在GetxController的构造方法或onInit注册,需要在dispose方法中调用Worker的dispose方法销毁Worker对象。

debounce钩子函数

定义如下:
// 在限定的时间内只会执行一次指定的回调;在限定时间状态持续变化时不会执行callback,超过限定时间内没有变化时才执行callback
Worker debounce<T>(
  RxInterface<T> listener,  // 状态变量
  WorkerCallback<T> callback, {  // 状态变量改变时的回调
  Duration? time,  // 限定时间,
  Function? onError,  
  void Function()? onDone,
  bool? cancelOnError,
});

例:
疯狂点击时不会调用,停下来1s后调用
debounce(_counter, (latestValue) => print("callback: $latestValue"), time: Duration(seconds: 1));

interval钩子函数

定义如下:
// 忽略间隔time时间范围内的变化,在状态变量发生变化且每隔 time时间就会调用1次callback。
Worker interval<T>(
  RxInterface<T> listener,
  WorkerCallback<T> callback, {
  Duration time = const Duration(seconds: 1),
  dynamic condition = true,  // 返回true时才会执行 callback
  Function? onError,
  void Function()? onDone,
  bool? cancelOnError,
})

例:
疯狂点击后每隔1s调用一次
class WorkerController extends GetxController {
  final _counter = 0.obs;
  set counter(value) => this._counter.value = value;
  get counter => this._counter.value;
  late Worker worker;
  int coinCount = 0;
  @override
  void onInit() {
    worker = interval(
      _counter,
      (_) {
        coinCount++;
        print("金币数: $coinCount");
      },
      time: Duration(seconds: 1),  
      condition: () => coinCount < 10,
    );
    super.onInit();
  }
  @override
  void dispose() {
    worker.dispose();
    super.dispose();
  }
}

其他钩子函数(调用方式和interval一样)

  1. once
    状态变量变化时只执行一次,比如详情页面的刷新只更新一次浏览次数。
  2. ever
    每次变化都执行,可以用于点赞这种场合。
  3. everAll
    用于列表类型状态变量,只要列表元素改变就会执行回调。

StateMixin (避免代码通过if-else显示不同UI)

使用
  class XXXController extends GetxController with StateMixin<T> {}

可以在状态数据中混入页面数据加载状态:
  1. RxStatus.loading():加载中;
  2. RxStatus.success():加载成功;
  3. RxStatus.error([String? message]):加载失败,可以携带一个错误信息 message;
  4. RxStatus.empty():无数据。
提供了一个change方法用于传递状态数据和状态给页面。
  // newState:新的状态数据;status: 上面的4种状态。该方法会通知 Widget 刷新。
  void change(T? newState, {RxStatus? status})

通过继承GetView使用controller.obx的4状态来构建界面。
GetView定义如下:
// 继承自StatelessWidget的抽象类
abstract class GetView<T> extends StatelessWidget {
  const GetView({Key? key}) : super(key: key);
  final String? tag = null;
  // 获取controller
  T get controller => GetInstance().find<T>(tag: tag)!;
  @override
  Widget build(BuildContext context);
}
controller.obx定义如下:
// NotifierBuilder的定义:typedef NotifierBuilder<T> = Widget Function(T state);
Widget obx(
  NotifierBuilder<T?> widget, {  // 正常状态的Widget构建函数(携带状态)
  Widget Function(String? error)? onError,  // 出错时的Widget构建函数
  Widget? onLoading, // 加载时的Widget构建函数
  Widget? onEmpty, // 数据为空时的Widget构建函数。
})

示例

class PersonalHomePageMixin extends GetView<PersonalMixinController> {
  PersonalHomePageMixin({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return controller.obx(
      (personalEntity) => _PersonalHomePage(personalProfile: personalEntity!),
      onLoading: Center(
        child: CircularProgressIndicator(),
      ),
      onError: (error) => Center(
        child: Text(error!),
      ),
      onEmpty: Center(
        child: Text('暂无数据'),
      ),
    );
  }
}
class PersonalMixinController extends GetxController
    with StateMixin<PersonalEntity> {
  final String userId;
  PersonalMixinController({required this.userId});
  @override
  void onReady() {
    getPersonalProfile(userId);
    super.onReady();
  }
  void getPersonalProfile(String userId) async {
    change(null, status: RxStatus.loading());
    var personalProfile = await JuejinService().getPersonalProfile(userId);
    if (personalProfile != null) {
      change(personalProfile, status: RxStatus.success());
    } else {
      change(null, status: RxStatus.error('获取个人信息失败'));
    }
  }
}

// 从 GetView 的源码可以看到,Controller 是从容器中获取的,这就需要使用 GetX 的容器,在使用Controller前注册到GetX容器中
Get.lazyPut<PersonalMixinController>(
  () => PersonalMixinController(userId: '70787819648695'),
);

依赖注入(在外部创建好依赖对象然后注入)

abstact class Coding {
  void coding();
}
class Coder implements Coding {
  final String name;
  final int age;
  final Gender gender;  
  Coder({required this.name, required this.age, required this.gender});
}
class CEO {
  late Coding nbCoder;
  // 直接外部传入,而不是内部去创建。
  CEO(this.nbCoder);
  void developProduct() {
    nbCoder.coding();
  }
}

IoC的容器

可以简单理解为是一个Map对象,类名作为键,具体对象作为值存储,这样就可以根据类名找到容器中对应的对象。

GetX提供了4个方法往容器存储对象:
  除了create方法每次都会创建新的依赖对象以外,其他默认都是单例的(除非更改 tag 参数)
  // 直接放入依赖对象。
  Get.put(S dependency, ...);
  // 懒加载方式,在使用时如果没有实例对象则调用builder构建。优先推荐该方式
  void lazyPut<S>(InstanceBuilderCallback<S> builder, ...);
  // 异步获取实例对象
  Future<S> putAsync<S>(AsyncInstanceBuilderCallback<S> builder, ...);
  // 每次构建新的实例对象
  void create<S>(InstanceBuilderCallback<S> builder, ...);
从GetX容器获取对象使用 find()方法,通过泛型 S 和 tag 可以精准获取容器的某一个实例对象。
  S find<S>({String? tag})
要在业务中使用容器中的对象前提是要在业务代码前向容器注册对象,因此实际使用中可以写一个容器注册类,统一在main方法中runApp方法之前完成容器对象注册。

例:
// 存
void main() {
  // 可以写一个容器注册类,统一在main 方法中完成容器对象注册。
  // 可通过返回不同的实现,来切换Moc数据和真实网络请求数据。
  Get.lazyPut<LotteryService>(() => LotteryServiceImpl()); 
  runApp(MyApp());
}
// 取
final LotteryController lotteryController =
      LotteryController(Get.find<LotteryService>());

GetX的缺点

1. 使用GetX的导航需要使用GetMeterialApp或GetCupertinoApp包裹应用才能够在页面跳转时无需使用BuildContext。这对应用的侵入性比较强。
2. SnackBar、对话框和导航的实现内部使用了静态的context,这使得单元测试没法直接完成,而需要使用widget testing配合完成。
3. get_connect 插件集成了 REST API 请求和 GraphQL 客户端。这有点多余,一般的应用不会二者都用,这会导致插件部分功能多余(推荐网络请求还是用 Dio)。
4. 依赖注入和热重载问题:GetX 的依赖注入还不太成熟,如果依赖对象改变后(比如修改了依赖对象类型,增加了依赖对象),直接热重载会报错,这个时候往往需要 reload 才行。
5. 注释和文档比较糟糕: 源码很多地方缺少注释,导致未来的维护可能会比较麻烦。而官方文档相对也不是很完善,导致使用者需要自己摸索。
6. 代码组织性比较差:如果去阅读源码,会发现一个文件会有数百行,多个类、函数、变量都混在一个文件中。同时部分方法的命名也需要改进,比如路由的 Get.to,Get.toNamed,Get.offNamed,依赖注入的 Get.put,Get.lazyPut 等,这些方法名称如果不阅读文档很难推断出具体的功能。如果在方法和 Get 之间加一个模块名称会更好理解,比如 Get.router.to,Get.dependencies.put。
7. 很多代码没有经过测试:由于 GetX 覆盖的功能实在太多,很难做到每个功能特性都做完善的测试,这会使得其中可能存在隐藏的 Bug。
8. 使用 GetX 的建议:建议只使用必要的模块,比如 GetX 的状态管理、响应式编程和依赖注入,而像导航和绑定这类的功能要慎用。

GetConnect网络请求

get_connect插件

工具类

表单验证工具,获取系统参数(平台类型,屏幕尺寸等)

相关文章

网友评论

    本文标题:Flutter了解之入门篇9-2(状态管理库)

    本文链接:https://www.haomeiwen.com/subject/zxmoortx.html