3.3 状态管理:Provider, BLoC

基础知识

在 Flutter 应用中,状态管理是一个核心概念。状态是指在应用程序生命周期中可能发生变化的数据。当状态发生变化时,UI 需要随之更新以反映这些变化。有效的状态管理策略对于构建可维护、可扩展和高性能的 Flutter 应用至关重要。

Flutter 本身提供了 StatefulWidget 来管理局部状态,但对于跨多个 Widget 共享的状态或复杂的状态逻辑,我们需要更强大的状态管理解决方案。目前 Flutter 社区有多种状态管理方案,其中 ProviderBLoC(Business Logic Component)是两种非常流行且功能强大的模式。

1. Provider (提供者)

Provider 是 Flutter 官方推荐的状态管理方案之一,它基于 InheritedWidget,但提供了更简洁、更易于使用的方式来管理和访问状态。Provider 的核心思想是“提供”数据给 Widget 树中的后代 Widget,并允许这些 Widget 在数据变化时重建。

  • 核心概念
    • Provider:最基本的提供者,用于提供一个值,当值改变时,依赖它的 Widget 会重建。
    • ChangeNotifierProvider:用于提供一个 ChangeNotifier 对象。当 ChangeNotifier 调用 notifyListeners() 时,依赖它的 Widget 会重建。
    • Consumer:一个 Widget,用于监听 Provider 提供的状态变化,并在状态变化时重建自身。
    • Selector:类似于 Consumer,但允许你只监听状态的特定部分,从而进行更细粒度的重建。
    • readwatch
      • context.read<T>():用于获取 Provider 提供的状态,但不会监听状态变化。适用于一次性读取或在事件回调中使用。
      • context.watch<T>():用于获取 Provider 提供的状态,并监听状态变化。当状态变化时,使用 watch 的 Widget 会重建。

示例:使用 ChangeNotifierProvider 管理计数器状态

dart
复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 1. 定义一个 ChangeNotifier 类来管理状态
class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // 通知所有监听器状态已改变
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider 状态管理示例'),
      ),
      body:
          // 2. 使用 ChangeNotifierProvider 提供 Counter 实例
          ChangeNotifierProvider(
        create: (context) => Counter(),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                '你点击了按钮这么多次:',
              ),
              // 3. 使用 Consumer 监听 Counter 的变化并显示
              Consumer<Counter>(
                builder: (context, counter, child) {
                  return Text(
                    '${counter.count}',
                    style: Theme.of(context).textTheme.headlineMedium,
                  );
                },
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  // 4. 通过 context.read<Counter>() 获取 Counter 实例并调用方法
                  context.read<Counter>().increment();
                },
                child: const Text('增加计数'),
              ),
              const SizedBox(height: 20),
              // 5. 使用 Selector 监听特定部分的状态
              Selector<Counter, bool>(
                selector: (context, counter) => counter.count % 2 == 0,
                builder: (context, isEven, child) {
                  return Text(
                    isEven ? '当前计数是偶数' : '当前计数是奇数',
                    style: TextStyle(color: isEven ? Colors.green : Colors.red),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: ProviderExampleScreen()));
}

2. BLoC (Business Logic Component)

BLoC 模式由 Google 提出,旨在将业务逻辑与 UI 分离,使代码更易于测试、复用和理解。BLoC 的核心思想是使用 Stream 来处理输入(Events)和输出(States)。

  • 核心概念
    • Events (事件):用户操作或外部触发的输入,例如按钮点击、数据加载请求等。Events 被发送到 BLoC。
    • BLoC (业务逻辑组件):接收 Events,处理业务逻辑,然后输出新的 States。BLoC 通常不直接依赖 Flutter Widget,而是纯 Dart 类。
    • States (状态):BLoC 处理完 Event 后产生的新状态。States 通过 StreamStreamController 暴露给 UI。
    • flutter_bloc:一个流行的 Flutter 包,提供了 BlocCubit 类以及 BlocProviderBlocBuilderBlocListener 等 Widget,极大地简化了 BLoC 模式的实现。

示例:使用 flutter_bloc 管理计数器状态

首先,在 pubspec.yaml 中添加 flutter_bloc 依赖:

yaml
复制代码
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3 # 最新版本可能不同

然后运行 flutter pub get

dart
复制代码
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// 1. 定义 Events
abstract class CounterEvent {}

class Increment extends CounterEvent {}

class Decrement extends CounterEvent {}

// 2. 定义 States
class CounterState {
  final int count;
  CounterState(this.count);

  @override
  List<Object> get props => [count]; // 用于比较状态是否相同
}

// 3. 定义 BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    // 注册事件处理器
    on<Increment>((event, emit) {
      emit(CounterState(state.count + 1)); // 发出新的状态
    });
    on<Decrement>((event, emit) {
      emit(CounterState(state.count - 1)); // 发出新的状态
    });
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BLoC 状态管理示例'),
      ),
      body:
          // 4. 使用 BlocProvider 提供 BLoC 实例
          BlocProvider(
        create: (context) => CounterBloc(),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                '你点击了按钮这么多次:',
              ),
              // 5. 使用 BlocBuilder 监听状态变化并显示
              BlocBuilder<CounterBloc, CounterState>(
                builder: (context, state) {
                  return Text(
                    '${state.count}',
                    style: Theme.of(context).textTheme.headlineMedium,
                  );
                },
              ),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () {
                      // 6. 通过 context.read<CounterBloc>() 获取 BLoC 实例并添加事件
                      context.read<CounterBloc>().add(Increment());
                    },
                    child: const Text('增加'),
                  ),
                  const SizedBox(width: 20),
                  ElevatedButton(
                    onPressed: () {
                      context.read<CounterBloc>().add(Decrement());
                    },
                    child: const Text('减少'),
                  ),
                ],
              ),
              const SizedBox(height: 20),
              // 7. 使用 BlocListener 监听状态变化并执行副作用 (如显示 SnackBar)
              BlocListener<CounterBloc, CounterState>(
                listener: (context, state) {
                  if (state.count % 5 == 0) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('计数达到 ${state.count}!')),
                    );
                  }
                },
                child: const SizedBox.shrink(), // BlocListener 通常不需要显示 UI
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: BlocExampleScreen()));
}

官方文档链接

Flutter 开发中的应用案例

状态管理是 Flutter 应用开发中不可避免的话题。选择合适的状态管理方案取决于项目的规模、复杂度和团队偏好。Provider 适用于中小型应用或简单的状态共享,而 BLoC 则更适合大型、复杂的应用,它强制了业务逻辑与 UI 的分离,提高了代码的可测试性和可维护性。

案例:一个简单的待办事项应用 (结合 Provider 和 BLoC 的思想)

我们将创建一个简单的待办事项应用,演示如何使用 ChangeNotifierProvider 来管理待办事项列表的状态,并结合一些 BLoC 的思想,将添加/删除待办事项的逻辑封装在 ChangeNotifier 中。

dart
复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 待办事项模型
class Todo {
  String id;
  String title;
  bool isCompleted;

  Todo({required this.id, required this.title, this.isCompleted = false});

  // 复制构造函数,用于创建新的 Todo 实例(不可变性)
  Todo copyWith({String? id, String? title, bool? isCompleted}) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}

// 待办事项管理类 (ChangeNotifier)
class TodoListNotifier with ChangeNotifier {
  final List<Todo> _todos = [];

  List<Todo> get todos => List.unmodifiable(_todos); // 返回不可修改的列表

  void addTodo(String title) {
    if (title.trim().isEmpty) return;
    _todos.add(Todo(id: DateTime.now().toIso8601String(), title: title));
    notifyListeners();
  }

  void toggleTodoCompletion(String id) {
    final index = _todos.indexWhere((todo) => todo.id == id);
    if (index != -1) {
      _todos[index] = _todos[index].copyWith(isCompleted: !_todos[index].isCompleted);
      notifyListeners();
    }
  }

  void removeTodo(String id) {
    _todos.removeWhere((todo) => todo.id == id);
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('待办事项'),
      ),
      body:
          // 提供 TodoListNotifier 实例
          ChangeNotifierProvider(
        create: (context) => TodoListNotifier(),
        child: Column(
          children: <Widget>[
            _AddTodoInput(), // 添加待办事项输入框
            Expanded(
              // 监听 TodoListNotifier 的变化并构建列表
              child: Consumer<TodoListNotifier>(
                builder: (context, todoListNotifier, child) {
                  return ListView.builder(
                    itemCount: todoListNotifier.todos.length,
                    itemBuilder: (context, index) {
                      final todo = todoListNotifier.todos[index];
                      return ListTile(
                        title: Text(
                          todo.title,
                          style: TextStyle(
                            decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
                            color: todo.isCompleted ? Colors.grey : Colors.black,
                          ),
                        ),
                        leading: Checkbox(
                          value: todo.isCompleted,
                          onChanged: (bool? newValue) {
                            todoListNotifier.toggleTodoCompletion(todo.id);
                          },
                        ),
                        trailing: IconButton(
                          icon: const Icon(Icons.delete),
                          onPressed: () {
                            todoListNotifier.removeTodo(todo.id);
                          },
                        ),
                      );
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _AddTodoInput extends StatefulWidget {
  @override
  State<_AddTodoInput> createState() => _AddTodoInputState();
}

class _AddTodoInputState extends State<_AddTodoInput> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextField(
              controller: _controller,
              decoration: const InputDecoration(
                labelText: '添加新待办事项',
                border: OutlineInputBorder(),
              ),
              onSubmitted: (_) => _addTodo(),
            ),
          ),
          const SizedBox(width: 8),
          ElevatedButton(
            onPressed: _addTodo,
            child: const Text('添加'),
          ),
        ],
      ),
    );
  }

  void _addTodo() {
    // 通过 context.read 获取 TodoListNotifier 实例并调用 addTodo 方法
    context.read<TodoListNotifier>().addTodo(_controller.text);
    _controller.clear();
  }
}

void main() {
  runApp(const MaterialApp(home: TodoAppScreen()));
}

案例分析:

  • Todo 模型:定义了待办事项的数据结构,包含 idtitleisCompletedcopyWith 方法用于在修改 Todo 实例时保持不可变性,这是函数式编程和状态管理中的一个好实践。
  • TodoListNotifier (ChangeNotifier):这个类继承自 ChangeNotifier,负责管理待办事项列表的实际状态 (_todos)。它提供了 addTodotoggleTodoCompletionremoveTodo 等方法来修改列表。每次修改后,都会调用 notifyListeners() 来通知所有监听器(即 Consumer Widget)重建。
  • ChangeNotifierProvider:在 TodoAppScreenbuild 方法中,我们使用 ChangeNotifierProvider 来创建并提供 TodoListNotifier 的实例。这样,TodoAppScreen 的所有后代 Widget 都可以访问到这个 TodoListNotifier
  • Consumer<TodoListNotifier>:在 Expanded Widget 中,我们使用 Consumer 来监听 TodoListNotifier 的变化。当 TodoListNotifier 调用 notifyListeners() 时,Consumerbuilder 方法会被调用,从而重建 ListView.builder,显示最新的待办事项列表。
  • context.read<TodoListNotifier>().addTodo(_controller.text):在 _AddTodoInput_addTodo 方法中,我们使用 context.read 来获取 TodoListNotifier 的实例,并调用 addTodo 方法。这里使用 read 而不是 watch 是因为我们只是想触发一个事件(添加待办事项),而不需要 _AddTodoInputTodoListNotifier 状态变化时重建。
  • List.unmodifiable(_todos):在 TodoListNotifiertodos getter 中,我们返回了一个不可修改的列表。这是一种防御性编程,可以防止外部直接修改 _todos 列表,从而确保所有状态修改都通过 TodoListNotifier 的方法进行,便于追踪和管理。

这个案例展示了如何使用 Provider 模式来管理 Flutter 应用中的复杂状态。通过将业务逻辑封装在 ChangeNotifier 中,并使用 ProviderConsumer 来连接 UI 和状态,我们可以构建出清晰、可维护且响应迅速的 Flutter 应用。虽然这里没有直接使用 flutter_bloc 包,但 TodoListNotifier 的设计思想(封装业务逻辑、通过方法触发状态改变、通知 UI)与 BLoC 模式的核心原则是相通的。