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

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

作者: 平安喜乐698 | 来源:发表于2022-09-19 17:50 被阅读0次
目录

  1. GetX(流行度超Provider)
  2. BLoC (状态管理库中Star最高)
  3. RepositoryProvider
  4. GetIt

1. GetX

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

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

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

缺陷
  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();
    },
  ),
);

方式2. 响应式状态管理

响应式状态管理相当于实现了状态对象的绑定,只要状态对象发生了改变,依赖状态对象的组件会自动刷新,而不需要手动调用 upade 刷新。

优点:
  1. 无需创建 StreamController;
  2. 无需为每个变量创建 StreamBuilder;
  3. 无需为每个状态创建一个类;
  4. 无需为一个初始值创建一个 get 方法;
  5. 使用GetX响应式编程非常简单,就像使用 setState 一样。


声明状态变量(Rx变量)的3种方式
  1. Rx{Type}
  /*
  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 = ''.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)

监测变量
// 在GetX中为String对象创建了一个 Stream,赋予了初始值,然后会通知所有使用该对象的 Widget。一旦这个对象的值发生了改变,就会刷新这些组件。这是通过 Obx 组件实现的。
// 和setState有个区别,GetX 扩展的.obs 用法内部做了是否相等的比较,如果更新操作前后的对象是相等的话,那么不会通知组件刷新,从而提高性能。对于一个Controller拥有多个对象的时候,当这些对象发生改变的时候,也只会更新那些依赖这些对象的组件,而不是所有依赖 Controller 的组件。
var name = 'hello'.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 = 'island-coder';
        },
      ),
    );
  }
}

定向刷新

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

看一下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的组件
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,
  ),
),

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

debounce钩子函数定义如下:
// 在限定的时间内只会执行一次指定的回调
// 在限定时间状态持续变化时不会执行callback,超过限定时间内没有变化时才执行callback
Worker debounce<T>(
  RxInterface<T> listener,  // 状态变量
  WorkerCallback<T> callback, {  // 状态变量改变时的回调
  Duration? time,  // 限定时间,
  Function? onError,  
  void Function()? onDone,
  bool? cancelOnError,
});
例: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,
})
例:
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),  // 疯狂点击后每隔1s调用一次callback
      condition: () => coinCount < 10,
    );
    super.onInit();
  }
  @override
  void dispose() {
    worker.dispose();
    super.dispose();
  }
}

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

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

StateMixin

可以在状态数据中混入页面数据加载状态:
  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})

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


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);
}
通过继承 GetView,就可以直接使用controller.obx构建界面。
controller.obx定义如下:
Widget obx(
  // typedef NotifierBuilder<T> = Widget Function(T state);
  NotifierBuilder<T?> 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('获取个人信息失败'));
    }
  }
}

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() {
  Get.lazyPut<LotteryService>(() => LotteryServiceImpl()); 
  runApp(MyApp());
}
// 取
final LotteryController lotteryController =
      LotteryController(Get.find<LotteryService>());

GetConnect网络请求

get_connect插件

工具类

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

2. BLoC (状态管理库中Star最高)

BLoC更多的是一种设计模式,按照这种设计模式可以转变为很多种状态管理实现。
基于Stream / Observable 范式。

bloc、flutter_bloc插件
bloc_package插件是为了快速在 Flutter 或 Dart 中实现 BLoC 模式的插件。
BLoC 依赖 Stream和 StreamController实现,组件通过Sinks发送更新状态的事件,然后再通过 Streams 通知其他组件更新。事件处理和通知刷新的业务逻辑都是由 BLoC 完成,从而实现业务逻辑与 UI 层的分离(有点类似 Redux),并且逻辑部分可以复用和可以单独进行单元测试。

3个重要概念

1. Cubit
Cubit是管理状态数据的 BlocBase 子类,它可以管理任意类型的数据,包括基本类型到复杂对象。
Cubit调用emit构建新的状态数据前需要给状态数据一个初始值。当状态数据发生改变时会触发onChange回调,出错时会触发onError回调。
UI界面可以通过调用 Cubit 对外暴露的更新状态方法触发状态更新,而在 onChange 中会得到更新前后的状态,从而可以触发界面刷新。 

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }
  @override
  void onError(Object error, StackTrace stackTrace) {
    print('$error, $stackTrace');
    super.onError(error, stackTrace);
  }
}
2. BlocObserver
BlocObserver可以同时监听所有的Cubit的变化

class CounterCubit extends Cubit<int> {
  CounterCubit({initial = 0}) : super(initial);
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}
class MyBlocObserver extends BlocObserver {
  @override
  void onCreate(BlocBase bloc) {
    print('BloC Observer onCreate:  ${bloc.state}');
    super.onCreate(bloc);
  }
  @override
  void onChange(BlocBase bloc, Change change) {
    print('BloC Observer onChange: $change');
    super.onChange(bloc, change);
  }
  @override
  void onClose(BlocBase bloc) {
    print('BloC Observer onClose: ${bloc.state}');
    super.onClose(bloc);
  }
  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('Bloc Observer onError: $error, $stackTrace');
    super.onError(bloc, error, stackTrace);
  }
  @override
  void onEvent(Bloc bloc, Object? event) {
    print('Bloc Observer onEvent: $event, ${bloc.state}');
    super.onEvent(bloc, event);
  }
}
void main() {
  Bloc.observer = MyBlocObserver();
  final cubit = CounterCubit();
  cubit.increment();
  print('after increment: ${cubit.state}');
  cubit.decrement();
  print('after decrement: ${cubit.state}');
  final anotherCubit = CounterCubit(10);
  anotherCubit.increment();
  cubit.close();
  anotherCubit.close();
}
/*
输出如下:
BloC Observer onCreate:  0
BloC Observer onChange: Change { currentState: 0, nextState: 1 }
BloC Observer onChange: Change { currentState: 1, nextState: 0 }
BloC Observer onCreate:  10
BloC Observer onChange: Change { currentState: 10, nextState: 11 }
BloC Observer onClose: 0
BloC Observer onClose: 11
*/
3. BLoC
Bloc也是继承BlocBase的类,比Cubit更高级。使用的是 events 而不是暴露的函数来更新状态。
在Bloc内部有一个onEvent方法,通过 EventTransformer将 event转换为更新状态的方法来刷新状态数据。每个event都可以有对应的 EventHandler来处理该 event,完成后再通过 emit 触发通知状态更新。当状态转变前会调用 onTransition,有当前的状态,触发更新的 event 和下一个状态。


abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc(int initialState) : super(initialState) {
    on<IncrementEvent>((event, emit) => emit(state + 1));
    on<DecrementEvent>((event, emit) => emit(state - 1));
  }
  @override
  void onTransition(Transition<CounterEvent, int> transition) {
    print(
        'Current: ${transition.currentState}, Next: ${transition.nextState}, Event: ${transition.event}');
    super.onTransition(transition);
  }
}
void main() {
  Bloc.observer = MyBlocObserver();
  final counterBloc = CounterBloc(5);
  counterBloc.add(IncrementEvent());
  counterBloc.add(DecrementEvent());
  counterBloc.close();
}
/*
输出如下:
Current: 5, Next: 6, Event: Instance of 'IncrementEvent'
BloC Observer onChange: Change { currentState: 5, nextState: 6 }
Current: 6, Next: 5, Event: Instance of 'DecrementEvent'
BloC Observer onChange: Change { currentState: 6, nextState: 5 }
BloC Observer onClose: 5
*/

实现一个简单状态管理(SimpleBLocProvider)

首先需要一个builder属性构建UI组件,且builder需要携带最新的state状态数据用来更新UI组件。
// 例:SimpleBlocProvider<int> (builder: (count) => Text('$count'),)
typedef StateBuilder<T> = Widget Function(T state);
还需要Bloc逻辑组件,来获取最新的状态数据。

定义如下:
class SimpleBlocProvider<T> extends StatefulWidget {
  final StateBuilder<T> builder;
  final BlocBase<T> bloc;
  const SimpleBlocProvider(
      {Key? key, required this.builder, required this.bloc})
      : super(key: key);
  @override
  _SimpleBlocProviderState<T> createState() => _SimpleBlocProviderState<T>();
}
要实现BLoC刷新,需要监听BLoC状态数据的变化。BLoC 是基于 Stream 实现的,对于 Stream,可以使用其 listen 方法来监听 Stream 流数据的变化。listen 方法定义如下:
StreamSubscription<T> listen(void onData(T event)?, {Function? onError, void onDone()?, bool? cancelOnError});
可以在 listen 的 onData 中调用 setState 刷新界面。组件销毁时需要取消监听,在_SimpleBlocProviderState 中定义一个属性_streamSubscription存储 listen 方法的返回值,并在 dispose 中取消监听。

_streamSubscription = widget.bloc.stream.listen((data) {
  setState(() {
    _state = data;
  });
});
@override
Widget build(BuildContext context) {
  return widget.builder(_state);
}
@override
void dispose() {
  _streamSubscription.cancel();
  super.dispose();
}
完整代码如下

typedef StateBuilder<T> = Widget Function(T state);
class SimpleBlocProvider<T> extends StatefulWidget {
  final StateBuilder<T> builder;
  final BlocBase<T> bloc;
  const SimpleBlocProvider(
      {Key? key, required this.builder, required this.bloc})
      : super(key: key);
  @override
  _SimpleBlocProviderState<T> createState() => _SimpleBlocProviderState<T>();
}
class _SimpleBlocProviderState<T> extends State<SimpleBlocProvider<T>> {
  late T _state;
  late StreamSubscription<T> _streamSubscription;
  @override
  void initState() {
    _state = widget.bloc.state;
    super.initState();
    _streamSubscription = widget.bloc.stream.listen((data) {
      setState(() {
        _state = data;
      });
    });
  }
  @override
  Widget build(BuildContext context) {
    return widget.builder(_state);
  }
  @override
  void dispose() {
    _streamSubscription.cancel();
    super.dispose();
  }
}

==================
使用
class CounterCubit extends Cubit<int> {
  CounterCubit({initial = 0}) : super(initial);
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
  @override
  void onChange(Change<int> change) {
    super.onChange(change);
  }
}
class SimpleBlocCounterPage extends StatelessWidget {
  final counter = CounterCubit();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc 计数器'),
      ),
      body: Center(
        child: SimpleBlocProvider<int>(
          builder: (count) => Text(
            '$count',
            style: TextStyle(
              fontSize: 32,
              color: Colors.blue,
            ),
          ),
          bloc: counter,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counter.increment();
        },
        tooltip: '点击增加',
        child: Icon(Icons.add),
      ),
    );
  }
}

===========================
使用2(Bloc)

class Person {
  final String name;
  final String gender;
  const Person({required this.name, required this.gender});
}
abstract class PersonEvent {}
class UsingCnNameEvent extends PersonEvent {}
class UsingEnNameEvent extends PersonEvent {}
class PersonBloc extends Bloc<PersonEvent, Person> {
  PersonBloc(Person person) : super(person) {
    on<UsingCnNameEvent>(
        (event, emit) => emit(Person(name: '狗', gender: '雄性')));
    on<UsingEnNameEvent>(
        (event, emit) => emit(Person(name: 'Dog', gender: 'male')));
  }
}
class SimpleBlocCounterPage extends StatelessWidget {
  final personBloc = PersonBloc(Person(name: '狗', gender: '雄性'));
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc 事件'),
      ),
      body: Center(
        child: SimpleBlocProvider<Person>(
          builder: (person) => Text(
            '姓名:${person.name},性别:${person.gender}',
            style: TextStyle(
              fontSize: 22,
              color: Colors.blue,
            ),
          ),
          bloc: personBloc,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          personBloc.add(UsingEnNameEvent());
        },
        tooltip: '点击增加',
        child: Icon(Icons.add),
      ),
    );
  }
}

flutter_bloc插件中的BlocProvider

示例

class BlocCounterWrapper extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: BlocCounterPage(),
    );
  }
}
class BlocCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc 计数器'),
      ),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) => Text(
            '$count',
            style: TextStyle(
              fontSize: 32,
              color: Colors.blue,
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<CounterCubit>().increment();
        },
        tooltip: '点击增加',
        child: Icon(Icons.add),
      ),
    );
  }
}


和Provider用法类似,作者为同一人。

final counter =  CounterCubit();
BlocProvider.value(
  value: counter,
  child: SomeWidget(),
);

final isPositive = context.select((CounterBloc b) => b.state >= 0);

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)

区别:
  BlocProvider是通过Stream.listen方法来更新,状态对象不需要实现 ChangeNotifier 接口,在状态数据改变的时候也无需调用 notifyListeners 来通知刷新。
  BlocProvider 可以看成是使用了 Stream.listen 监听状态数据的方式替换 ChangeNotifer 的 Provider 的变种。

BlocBuilder

flutter_bloc插件源码中的BlocBuilder的定义如下:
class BlocBuilder<B extends BlocBase<S>, S> extends BlocBuilderBase<B, S> {
  const BlocBuilder({
    Key? key,
    required this.builder,
    B? bloc,
    BlocBuilderCondition<S>? buildWhen,
  }) : super(key: key, bloc: bloc, buildWhen: buildWhen);
  final BlocWidgetBuilder<S> builder;
  @override
  Widget build(BuildContext context, S state) => builder(context, state);
}

// BlocBuilder 既可以配合 BlocProvider 在组件树中使用 Bloc 对象,也可以单独拥有自己的Bloc对象。
绑定状态对象有两种方式:
1. 在没有指定 bloc参数的时候,它会通过 BlocProvider 和 context自动向上寻找匹配的状态对象。这个代码在其父类BlocBuilderBase(是一个 StatefulWidget)的 State 对象中实现,实际上使用的还是 context.read 来完成的。
@override
void initState() {
  super.initState();
  _bloc = widget.bloc ?? context.read<B>();
  _state = _bloc.state;
}
2. 如果指定了 bloc 参数,那么就使用指定的 bloc 对象,这样可以使用自有的 bloc 对象而无需 BlocProvider 提供。这个用法有点像GetX 的 GetBuilder了。

按条件刷新
buildWhen参数:返回bool值的回调方法。以根据前后状态来决定是否要刷新界面。
typedef BlocBuilderCondition<S> = bool Function(S previous, S current);

示例(BlocBuilder)

class PersonalBloc extends Bloc<PersonalEvent, PersonalResponse> {
  final String userId;
  PersonalEntity? _personalProfile;
  PersonalBloc(PersonalResponse initial, {required this.userId})
      : super(initial) {
    on<FetchEvent>((event, emit) {
      getPersonalProfile(userId);
    });
    on<FetchSucessEvent>((event, emit) {
      emit(PersonalResponse(
        personalProfile: _personalProfile,
        status: LoadingStatus.success,
      ));
    });
    on<FetchFailedEvent>((event, emit) {
      emit(PersonalResponse(
        personalProfile: null,
        status: LoadingStatus.failed,
      ));
    });
    on<RefreshEvent>((event, emit) {
      getPersonalProfile(userId);
    });
    add(FetchEvent());
  }
  void getPersonalProfile(String userId) async {
    _personalProfile = await JuejinService().getPersonalProfile(userId);
    if (_personalProfile != null) {
      add(FetchSucessEvent());
    } else {
      add(FetchFailedEvent());
    }
  }
}
class PersonalHomePage extends StatelessWidget {
  PersonalHomePage({Key? key}) : super(key: key);
  final personalBloc = PersonalBloc(
      PersonalResponse(
        personalProfile: null,
        status: LoadingStatus.loading,
      ),
      userId: '70787819648695');
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PersonalBloc, PersonalResponse>(
      bloc: personalBloc,
      builder: (_, personalResponse) {
        print('build PersonalHomePage');
        if (personalResponse.status == LoadingStatus.loading) {
          return Center(
            child: Text('加载中...'),
          );
        }
        if (personalResponse.status == LoadingStatus.failed) {
          return Center(
            child: Text('请求失败'),
          );
        }
        PersonalEntity personalProfile = personalResponse.personalProfile!;
        return Stack(
          children: [
            CustomScrollView(
              slivers: [
                _getBannerWithAvatar(context, personalProfile),
                _getPersonalProfile(personalProfile),
                _getPersonalStatistic(personalProfile),
              ],
            ),
            Positioned(
              top: 40,
              right: 10,
              child: IconButton(
                onPressed: () {
                  personalBloc.add(FetchEvent());
                },
                icon: Icon(
                  Icons.refresh,
                  color: Colors.white,
                ),
              ),
            ),
          ],
        );
      },
      buildWhen: (previous, next) {
        if (previous.personalProfile == null || next.personalProfile == null) {
          return true;
        }
        return previous.personalProfile!.userId != next.personalProfile!.userId;
      },
    );
  }
  
  // 其他代码略
}

flutter_bloc插件提供了一个状态监听组件 BlocListener,当状态发生改变时会调用listener参数给定的回调函数(没有返回值),可用来处理一些提醒,例如显示弹窗提醒或确认、显示状态信息。
后置拦截器效果,在状态改变后做一些额外的的处理

登录状态(已登陆、已退出、退出确认)
enum LoginStatus { logon, logout, logoutConfirm }
class LoginCubit extends Cubit<LoginStatus> {
  LoginCubit({initial = LoginStatus.logout}) : super(initial);
  void login() => emit(LoginStatus.logon);
  void logout() => emit(LoginStatus.logout);
  void logoutConfirm() => emit(LoginStatus.logoutConfirm);
}

按钮和 BlocListener 都需要使用状态数据,因此使用 BlocProvider 放置在上层为 BlocListener 和 BlocBuilder 同时提供状态数据。
class BlocListenerWrapper extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => LoginCubit(),
      child: BlocListenerDemo(),
    );
  }
}

BlocListener部分代码如下:
class BlocListenerDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BlocListener 示例'),
      ),
      body: Center(
        child: BlocListener<LoginCubit, LoginStatus>(
          listener: (context, loginSatus) async {
            if (loginSatus == LoginStatus.logout ||
                loginSatus == LoginStatus.logon) {
              ScaffoldMessenger.of(context)
                ..hideCurrentSnackBar()
                ..showSnackBar(SnackBar(
                  content:
                      Text(loginSatus == LoginStatus.logout ? '已退出登录' : '登录成功'),
                  duration: Duration(seconds: 1),
                ));
            } else {
              var confirmed = await _confirmLogout(context);
              if (confirmed == true) {
                context.read<LoginCubit>().logout();
              }
            }
          },
          child: BlocBuilder<LoginCubit, LoginStatus>(
            builder: (context, loginSatus) => TextButton(
              child: Text(
                loginSatus == LoginStatus.logon ? '退出登录' : '登录',
                style: TextStyle(
                  fontSize: 24.0,
                ),
              ),
              onPressed: () {
                if (loginSatus == LoginStatus.logon) {
                  context.read<LoginCubit>().logoutConfirm();
                } else {
                  context.read<LoginCubit>().login();
                }
              },
            ),
          ),
        ),
      ),
    );
  }
BlocBuilder构建的页面按说不应该是 BlocListener 的子组件,而是同级结构
flutter_bloc 给出了 BlocConsumer来解决。BlocConsumer 像是 BlocBuilder和 BlocListener 的聚合组件,支持构建响应式组件的同时监听状态变化。同时也支持按条件刷新组件或响应状态变化监听回调。

BlocConsumer定义如下:
const BlocConsumer({
  Key? key,
  required this.builder,  // 
  required this.listener, // 
  this.bloc,  // 会自动从当前的BuildContext 中查找对应类型的状态对象
  this.buildWhen, // 接收前后的状态对象,返回 bool 值,若为 true 才会刷新组件
  this.listenWhen, // 接收前后的状态对象,返回 bool 值,若为 true 才会调用listener回调
}) : super(key: key);

BlocConsumer的builder方法实现如下:
@override
Widget build(BuildContext context) {
  if (widget.bloc == null) context.select<B, int>(identityHashCode);
  return BlocBuilder<B, S>(
    bloc: _bloc,
    builder: widget.builder,
    buildWhen: (previous, current) {
      if (widget.listenWhen?.call(previous, current) ?? true) {
        widget.listener(context, current);
      }
      return widget.buildWhen?.call(previous, current) ?? true;
    },
  );
}

3. RepositoryProvider

父子组件传值的方式:
  1. 构造函数传值
    父组件将子组件需要的对象通过构造函数传递给子组件;
    如果组件嵌套很深,传递数据对象需要层层传递,将导致代码很难维护。
  2. 单例对象
    构建单例对象,使得父子组件使用的是同一个对象;
    需要自己构建单例类,而实际上要传递的对象可能存在很多个实例。
  3. 容器
    将对象存入容器中,父子组件使用的时候直接从容器中获取。
    如果往容器存储不定数量的实例对象是不合适的。

flutter_bloc插件提供了一种基于组件的依赖注入方式解决这类问题,通过使用 RepositoryProvider,可以为组件树的子组件提供共享对象,这个共享对象只限在组件树中使用,可以通过 Provider 的方式访问该对象。

Repository 实际上是 Provider 的一个子类,通过注册单例的方式实现组件树对象共享,因此其注册的对象会随着 Provider 的注销而销毁,而且这个对象无需是 Bloc 子类。因此在无法使用 Bloc 传输共享对象的时候,可以使用 RepositoryProvider 来完成。
创建对象共享的两种方式
  create方式(通过调用一个方法创建新的对象)
  value方式(共享一个已有的对象)
RepositoryProvider定义如下:
class RepositoryProvider<T> extends Provider<T>
    with RepositoryProviderSingleChildWidget {
  RepositoryProvider({
    Key? key,
    required Create<T> create,
    Widget? child,
    bool? lazy,
  }) : super(
          key: key,
          create: create,
          dispose: (_, __) {},
          child: child,
          lazy: lazy,
        );
  RepositoryProvider.value({
    Key? key,
    required T value,
    Widget? child,
  }) : super.value(
          key: key,
          value: value,
          child: child,
        );
  static T of<T>(BuildContext context, {bool listen = false}) {
    try {
      // 实际上 RepositoryProvider就是 Provider,只是将静态方法 of 的listen 参数默认设置为 false 了,也就是不监听状态对象的变化。
      return Provider.of<T>(context, listen: listen);
    } on ProviderNotFoundException catch (e) {
      if (e.valueType != T) rethrow;
      throw FlutterError(
        '''
        RepositoryProvider.of() called with a context that does not contain a repository of type $T.
        No ancestor could be found starting from the context that was passed to RepositoryProvider.of<$T>().
        This can happen if the context you used comes from a widget above the RepositoryProvider.
        The context used was: $context
        ''',
      );
    }
  }
}

RepositoryProviderSingleChildWidget是一个空的 Mixin。是为了方便 MultiRepositoryProvider推断RepositoryProvider的类型。
mixin RepositoryProviderSingleChildWidget on SingleChildWidget {}


如果有多个对象需要共享,可以使用MultiRepositoryProvider
MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      create: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      create: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
)


子组件中访问共享对象
  // 方式1
  context.read<T>()
  // 方式2
  RepositoryProvider.of<T>(context)

实际上 RepositoryProvider 借用Provider 实现了一个组件树上的局部共享对象容器。通过这个容器,为RepositoryProvider的子组件树注入了共享对象,使得子组件可以从 context 中或使用RepositoryProvider.of 静态方法获取共享对象。通过这种方式避免了组件树的层层传值,使得代码更为简洁和易于维护。

RepositoryProvider.value(
  child: CustomScrollView(
    slivers: [
      const BannerWithAvatar(),
      const PersonalProfile(),
      const PersonalStatistic(),
    ],
  ),
  value: personalProfile,
),
使用context.read<PersonalEntity>()就可以从 RepositoryProvider 中取出personalProfile对象了

4. GetIt

是一个基于状态管理的服务管理工具,优点是不需要 BuildContext。如果想使用依赖注入、面向接口编程的方式来实现代码解耦和应用管理则十分适合。

最初的设计是用于完成依赖注入DI 和 IOC 容器的功能,有点类似Java Spring 的Bean容器。
GetIt中的对象是全局的,因此可以用来做数据同步。
对Flutter的最低SDK版本有要求(如版本7.1.2,需要SDK2.12.x以上版本)

注册
  // 注册对象:一般是单例
  GetIt.instance.registerSingleton<T>(T object);
  // 懒加载方式注册
  GetIt.instance.registerLazySingleton<T>(FactoryFunc<T> func)
获取
  // 获取容器中的对象
  GetIt.instance.get<T>();

示例

====================
dynamic_listener.dart文件

// 除了DynamicPage类外其他地方也可能有如下逻辑需求,所以抽离出
import 'package:home_framework/models/dynamic_entity.dart';
abstract class DynamicListener {
  // 更新新闻后
  void dynamicUpdated(String id, DynamicEntity updatedDynamic);
  // 添加新闻后
  void dynamicAdded(DynamicEntity newDynamic);
}


====================
dynamic_page.dart文件

class _DynamicPageState extends State<DynamicPage> implements DynamicListener {
  @override
  void initState() {
    super.initState();
    // 注册到GetIt容器,在其他页面可获取后调用更新或添加方法来更新本页面
    GetIt.instance.registerSingleton<DynamicListener>(this);
  }
  // 更新新闻的具体实现
  void dynamicUpdated(String id, DynamicEntity updatedDynamic) {
    int index = _listItems.indexWhere((element) => element.id == id);
    if (index != -1) {
      setState(() {
        _listItems[index] = updatedDynamic;
      });
    }
  }
  // 添加新闻的具体实现
  void dynamicAdded(DynamicEntity newDynamic) {
    setState(() {
      _listItems.insert(0, newDynamic);
    });
  }
}

========================
其他页面

// 新增页面
var response = await DynamicService.post(newFormData);
if (response.statusCode == 200) {
  Dialogs.showInfo(context, '添加成功');
  GetIt.instance
      .get<DynamicListener>()
      .dynamicAdded(DynamicEntity.fromJson(response.data));
  Navigator.of(context).pop();
}
// 编辑页面
if (response.statusCode == 200) {
  Dialogs.showInfo(context, '保存成功');
  //处理成功更新后的业务
  _handleUpdated(newFormData);
  Navigator.of(context).pop();
}
// 处理更新,如果图片更新了才更新动态图片内容
void _handleUpdated(Map<String, String> newFormData) {
  _dynamicEntity.title = newFormData['title'];
  _dynamicEntity.content = newFormData['content'];
  if (newFormData.containsKey('imageUrl')) {
    _dynamicEntity.imageUrl = newFormData['imageUrl'];
  }
  GetIt.instance.get<DynamicListener>().dynamicUpdated(
      _dynamicEntity.id,
      _dynamicEntity,
  );
}
// 详情页面
void _updateViewCount() async {
  try {
    var response = await DynamicService.updateViewCount(_dynamicEntity.id);
    if (response.statusCode == 200) {
      setState(() {
        _dynamicEntity.viewCount = response.data['viewCount'];
        GetIt.instance.get<DynamicListener>().dynamicUpdated(
              _dynamicEntity.id,
              _dynamicEntity,
            );
      });
    }
  } catch (e) {
    print(e.toString());
  }
}

相关文章

网友评论

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

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