Flutter响应式状态管理插件reactive_cubit的使用

Flutter响应式状态管理插件reactive_cubit的使用

1. 目标

开发一个Flutter包用于管理状态,而无需使用pub.dev上已有的任何状态管理包。该包应:

  • 支持高效的状态管理并易于集成到各种规模的应用程序中。
  • 通过仅在必要时更新UI组件来优化性能。
  • 提供友好且直观的API以方便集成。

2. 设计需求与目标

设计需求:
  • 清晰且可扩展的架构,支持从小型到复杂应用的各种场景。
  • 支持简单的状态(单个变量)和复杂的状态(数组、映射或嵌套结构)。
功能需求:
  • 提供以下方法:
    • 初始化状态。
    • 更新、更改和监听状态。
    • 重置或删除状态。
  • 确保UI组件仅在状态变化时重新渲染以优化性能。
  • 支持同步和异步状态更新。

3. 关于RxCubit的简介

a. 概述

RxCubitCubit 模式的启发,并利用了 rxdart 包中的 BehaviorSubject 来有效地管理状态。与复杂的第三方状态管理库不同,RxCubit 使用流 (BehaviorSubject) 来跟踪和广播状态的变化,从而优化UI更新。

b. 为什么RxCubit高效且优化
  • 使用 RxDart 可以轻松处理异步操作。
  • BehaviorSubject 会保留最后广播的值并将其重新发送给新的订阅者,这在组件初始化时获取状态值时非常有用。
  • 使用 distinct() 方法确保只有当状态真正改变时才会更新,减少了不必要的UI重渲染。
  • select() 方法允许只监听状态的特定部分,避免过度监听并优化资源使用。
c. RxCubit的模式

RxCubit Pattern

UI组件直接调用Cubit的方法,而不是创建事件(如BLoC模式),这简化了实现并保证了应用程序的高性能。状态由Cubit发出,UI监听状态并相应地更新。

4. RxCubit的实现方式

a. 创建Cubit
RxCubit(State initialState)
    : _initialState = initialState,
      _controller = BehaviorSubject<State>.seeded(initialState) {
  observer?.onCubitInit(this);
}
  • 在创建 RxCubit 时,BehaviorSubject 会被初始化为初始状态。
  • 可选的 observer 可用于在Cubit创建时进行调试或统计(例如打印日志)。
b. 访问当前状态流
Stream<State> get stream =>
    _controller.stream.doOnListen(_onListen).doOnCancel(_onCancel);
State get state => _controller.value;
  • stream: 返回状态的流,并通过 doOnListendoOnCancel 来跟踪监听器的数量。
  • state: 返回当前状态。
c. 发布状态变更
void emit(State newState) {
  if (!_controller.isClosed && state != newState) {
    observer?.onStateChanged(this, state, newState);
    _controller.add(newState);
  }
}
  • emit() 方法仅在新状态不同于当前状态时才发布新状态。这可以防止不必要的状态发布和UI重渲染。
d. 重置为初始状态
void reset() {
  if (!_controller.isClosed && state != _initialState) {
    observer?.onStateChanged(this, state, _initialState);
    _controller.add(_initialState);
  }
}
  • reset() 方法将状态重置为初始状态。这在需要清除或刷新状态时非常有用。
e. 选择状态的特定属性
Stream<T> select<T>(T Function(State state) selector) {
  return stream.map(selector).distinct().skip(1);
}
  • select() 方法允许监听状态的特定部分。它使用 distinct() 方法来过滤不必要的更改。
f. 自动关闭流
void _onListen() {
  _activeListeners++;
}

void _onCancel() {
  _activeListeners--;
  if (_activeListeners <= 0) {
    close();
  }
}
  • 跟踪活动监听器的数量。当没有监听器时,Cubit会自动关闭以释放资源,防止内存泄漏。

5. 使用RxCubit的好处

  • 高性能: 通过避免不必要的状态更新,减少UI重渲染。
  • 易于使用和扩展: 明确的API使得容易集成到现有项目中,而不需要大量重构。
  • 简单集成: 支持同步和异步状态更新,适用于复杂任务或API调用。

6. 结论

通过 RxCubit,你可以成功构建一个高效且易于使用的状态管理解决方案,而不依赖外部包。使用 RxDart,你可以通过流、选择器和合理过滤状态来有效管理状态。这种方案帮助Flutter应用保持高性能和良好的响应性。


StateObserver

StateObserver 用于监视 RxCubit 的生命周期和状态变化。它提供了处理状态变化、错误、初始化和取消Cubit的事件的方法。

主要方法:
  • onStateChanged<C extends RxCubit>(C cubit, dynamic previousState, dynamic newState)

    • 目的: 当Cubit的状态发生变化时被调用。
    • 参数:
      • cubit: 状态发生变化的 RxCubit 对象。
      • previousState: 变化前的状态。
      • newState: 变化后的状态。
  • onError<C extends RxCubit>(C cubit, Object error, StackTrace stackTrace)

    • 目的: 当Cubit发生错误时被调用。
    • 参数:
      • cubit: 错误发生的 RxCubit 对象。
      • error: 发生的错误。
      • stackTrace: 与错误相关的堆栈跟踪。
  • onCubitInit<C extends RxCubit>(C cubit)

    • 目的: 当Cubit被初始化时被调用。
    • 参数:
      • cubit: 正在初始化的 RxCubit 对象。
  • onCubitDispose<C extends RxCubit>(C cubit)

    • 目的: 当Cubit被销毁时被调用。
    • 参数:
      • cubit: 正在销毁的 RxCubit 对象。

RxMessageCubit

RxMessageCubit 继承自 RxCubit,用于明确地分离逻辑,以便更清晰地更新UI和显示对话框。

主要方法:
  • void sendMessage(M message)

    • 目的: 将消息发射到UI。
    • 参数:
      • message: 基于泛型类型的消息实例。
使用方法:
UI组件
late CounterMessageCubit cubit;

[@override](/user/override)
void initState() {
  super.initState();
  cubit = Provider.of<CounterMessageCubit>(context, listen: false);
  // 监听消息流以接收通知
  cubit.messageStream.listen(_onMessage);
}

void _onMessage(String message) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(message)),
  );
}
RxCubit
class CounterMessageCubit extends RxMessageCubit<int, String> {
  CounterMessageCubit() : super(0);

  void increment() {
    emit(state + 1);  // 在状态流上发射状态
    sendMessage('Counter incremented to $state');  // 在消息流上发射消息
  }

  void decrement() {
    emit(state - 1);
    sendMessage('Counter decremented to $state');
  }
}

使用示例

以下是使用 RxCubit 在Flutter应用中的一些简单示例。

void main() {
  runApp(
    RxCubitScope(
      observer: MyObserver(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  [@override](/user/override)
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentIndex = 0;

  // 底部导航栏将导航到的屏幕列表
  final List<Widget> _pages = [
    const SingleCubitExample(),
    const MultiCubitExample(),
  ];

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Navigation Example'),
      ),
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'SSE',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'MTS',
          ),
        ],
      ),
    );
  }
}

文件 single_cubit_provider_example.dart:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:reactive_cubit/reactive_cubit.dart';

import 'detail_screen.dart';

class CounterState {
  final bool isLoading;
  final int count;
  final bool isEven;

  CounterState(
      {required this.isLoading, required this.count, required this.isEven});

  CounterState copyWith({int? count, bool? isEven, bool? isLoading}) {
    return CounterState(
      count: count ?? this.count,
      isEven: isEven ?? this.isEven,
      isLoading: isLoading ?? false,
    );
  }

  [@override](/user/override)
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is CounterState &&
          runtimeType == other.runtimeType &&
          count == other.count &&
          isEven == other.isEven &&
          isLoading == other.isLoading;

  [@override](/user/override)
  int get hashCode => count.hashCode ^ isEven.hashCode;
}

class CounterCubit extends RxCubit<CounterState> {
  CounterCubit()
      : super(CounterState(count: 0, isEven: true, isLoading: false));

  void increment() async {
    emit(state.copyWith(isLoading: true));
    await Future.delayed(Durations.extralong4);
    final newCount = state.count + 1;
    emit(
      state.copyWith(count: newCount, isLoading: false),
    );
  }

  void updateLabel() {
    emit(
      state.copyWith(isEven: state.count % 2 == 0),
    );
  }
}

class SingleCubitExample extends StatelessWidget {
  const SingleCubitExample({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => CounterCubit(),
      child: const CounterWidget(),
    );
  }
}

class CounterWidget extends StatelessWidget {
  const CounterWidget({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    final cubit = Provider.of<CounterCubit>(context);

    return Stack(
      alignment: Alignment.center,
      children: [
        Column(
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const SizedBox(width: double.infinity),
            // 仅在 'count' 变化时重建
            StreamBuilder<int>(
              stream: cubit.select((state) => state.count),
              builder: (context, snapshot) {
                if (kDebugMode) {
                  print("Rebuild count");
                }
                return Text(
                  'Count: ${snapshot.data ?? 0}',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
            // 仅在 'isEven' 变化时重建
            StreamBuilder<bool>(
              stream: cubit.select((state) => state.isEven),
              builder: (context, snapshot) {
                if (kDebugMode) {
                  print("Rebuild title");
                }
                return Text(
                  snapshot.data == true ? 'Even' : 'Odd',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
            ElevatedButton(
              onPressed: cubit.increment,
              child: const Text('Increment'),
            ),
            ElevatedButton(
              onPressed: cubit.updateLabel,
              child: const Text('Update label'),
            ),
            ElevatedButton(
              onPressed: cubit.reset,
              child: const Text('Reset state'),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) {
                      return Provider(
                        create: (BuildContext context) => CounterMessageCubit(),
                        child: const DetailScreen(),
                      );
                    },
                  ),
                );
              },
              child: const Text('Go to detail'),
            ),
          ],
        ),
        Container(
          color: Colors.transparent,
          child: StreamBuilder<bool>(
            stream: cubit.select((state) => state.isLoading),
            builder: (context, snapshot) {
              if (kDebugMode) {
                print("Rebuild Loading");
              }
              return Offstage(
                offstage: !(snapshot.data ?? false),
                child: const IgnorePointer(
                  ignoring: true,
                  child: SizedBox.expand(
                    child: Center(
                      child: CircularProgressIndicator(),
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

更多关于Flutter响应式状态管理插件reactive_cubit的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter响应式状态管理插件reactive_cubit的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


reactive_cubit 是一个用于 Flutter 的响应式状态管理插件,它基于 cubitflutter_bloc,但提供了更简洁的 API 来处理响应式状态。reactive_cubit 允许你轻松地管理应用程序的状态,并且可以自动处理状态的更新和通知。

以下是 reactive_cubit 的基本使用方法:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 reactive_cubit 依赖:

dependencies:
  flutter:
    sdk: flutter
  reactive_cubit: ^0.1.0

然后运行 flutter pub get 来安装依赖。

2. 创建 ReactiveCubit

ReactiveCubit 是一个继承自 Cubit 的类,它允许你管理状态。你可以通过继承 ReactiveCubit 来创建自己的状态管理类。

import 'package:reactive_cubit/reactive_cubit.dart';

class CounterCubit extends ReactiveCubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

在这个例子中,CounterCubit 管理一个 int 类型的状态,并提供了 incrementdecrement 方法来更新状态。

3. 使用 ReactiveCubit 在 UI 中

你可以使用 ReactiveCubitBuilderReactiveCubitListener 来在 UI 中响应状态的变化。

import 'package:flutter/material.dart';
import 'package:reactive_cubit/reactive_cubit.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ReactiveCubitProvider(
        create: (context) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    final counterCubit = ReactiveCubitProvider.of<CounterCubit>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Counter Example')),
      body: Center(
        child: ReactiveCubitBuilder<CounterCubit, int>(
          builder: (context, state) {
            return Text('Count: $state');
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => counterCubit.increment(),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => counterCubit.decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

在这个例子中,ReactiveCubitProvider 用于在整个应用中提供 CounterCubit 实例。ReactiveCubitBuilder 用于在 UI 中响应状态的变化,并更新显示的内容。

4. 处理副作用

如果你需要在状态变化时执行一些副作用操作(例如导航、显示对话框等),可以使用 ReactiveCubitListener

class CounterPage extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    final counterCubit = ReactiveCubitProvider.of<CounterCubit>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Counter Example')),
      body: Center(
        child: ReactiveCubitListener<CounterCubit, int>(
          listener: (context, state) {
            if (state == 10) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Count reached 10!')),
              );
            }
          },
          child: ReactiveCubitBuilder<CounterCubit, int>(
            builder: (context, state) {
              return Text('Count: $state');
            },
          ),
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => counterCubit.increment(),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => counterCubit.decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

在这个例子中,当计数达到 10 时,ReactiveCubitListener 会显示一个 SnackBar。

5. 使用 ReactiveCubitSelector

ReactiveCubitSelector 允许你选择部分状态并在其变化时重建 UI。

class CounterPage extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    final counterCubit = ReactiveCubitProvider.of<CounterCubit>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Counter Example')),
      body: Center(
        child: ReactiveCubitSelector<CounterCubit, int, bool>(
          selector: (state) => state >= 10,
          builder: (context, isGreaterThanOrEqualTo10) {
            return Text('Is count >= 10? $isGreaterThanOrEqualTo10');
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => counterCubit.increment(),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => counterCubit.decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
回到顶部