当前位置: 首页 > ops >正文

Flutter BLoC 全面入门与实战(含代码示例)

下面是一篇系统、可实操的 Flutter BLoC 指南(含完整代码片段)。读完你可以快速上手从 CubitBloc、从 UI 到持久化与测试的全链路用法。


1. BLoC 是什么?为什么用?

BLoC(Business Logic Component)是一种把业务逻辑视图解耦的状态管理模式。
核心收益:

  • 逻辑可复用、可测试(Stream 驱动,输入事件 → 输出状态)

  • UI 更“傻”,只负责渲染;复杂度下沉到 Bloc/Cubit

  • 适合中大型项目(模块化、多人协作、依赖清晰)

快速对比:

  • Cubit:只有 State,没有 Event,API 更轻、更直接。

  • Bloc:Event + State,适合多来源事件、复杂流转。


2. 关键概念(很重要)

  • State:UI 渲染的数据快照,必须不可变(推荐配合 equatable)。

  • Event(仅 Bloc):触发状态变化的意图(用户点击、网络返回等)。

  • Cubit/Bloc:接收输入(事件/方法调用),输出新状态(Stream)。

  • BlocProvider:把 Bloc/Cubit 注入到 Widget 树。

  • BlocBuilder / BlocListener / BlocConsumer

    • Builder:根据状态重建 UI

    • Listener:根据状态执行一次性副作用(如弹 Toast)

    • Consumer:两者合体(慎用,通常拆开更清晰)

  • BlocSelector:只选状态的一部分,减少无关重建。


3. 准备工作(pubspec)

dependencies:flutter:sdk: flutterflutter_bloc: ^8.1.0       # 选择最新稳定版equatable: ^2.0.5rxdart: ^0.27.0            # 可选:做防抖/节流更方便# bloc_concurrency: ^0.2.0 # 可选:节流/丢弃等并发策略
dev_dependencies:bloc_test: ^9.1.0flutter_test:sdk: flutter

4. 示例一:用 Cubit 实现最小计数器

适合:简单状态,无需 Event

counter_cubit.dart

import 'package:flutter_bloc/flutter_bloc.dart';class CounterCubit extends Cubit<int> {CounterCubit() : super(0);void increment() => emit(state + 1);void decrement() => emit(state - 1);
}

ui.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_cubit.dart';class CounterPage extends StatelessWidget {const CounterPage({super.key});@overrideWidget build(BuildContext context) {return BlocProvider(create: (_) => CounterCubit(),child: Scaffold(appBar: AppBar(title: const Text('Cubit 计数器')),body: Center(child: BlocBuilder<CounterCubit, int>(builder: (_, count) => Text('$count', style: const TextStyle(fontSize: 48)),),),floatingActionButton: Column(mainAxisAlignment: MainAxisAlignment.end,children: [FloatingActionButton(onPressed: () => context.read<CounterCubit>().increment(), child: const Icon(Icons.add)),const SizedBox(height: 12),FloatingActionButton(onPressed: () => context.read<CounterCubit>().decrement(), child: const Icon(Icons.remove)),],),),);}
}

5. 示例二:用 Bloc 做一个 Todo(含 Repository、事件/状态)

5.1 模型与仓库

todo.dart

import 'package:equatable/equatable.dart';class Todo extends Equatable {final String id;final String title;final bool completed;const Todo({required this.id, required this.title, this.completed = false});Todo copyWith({String? title, bool? completed}) =>Todo(id: id, title: title ?? this.title, completed: completed ?? this.completed);@overrideList<Object?> get props => [id, title, completed];
}

todo_repository.dart

import 'dart:async';
import 'todo.dart';class TodoRepository {final List<Todo> _data = [];Future<List<Todo>> fetchTodos() async {await Future.delayed(const Duration(milliseconds: 300)); // 模拟网络return List.unmodifiable(_data);}Future<void> add(String title) async {_data.add(Todo(id: DateTime.now().microsecondsSinceEpoch.toString(), title: title));}Future<void> toggle(String id) async {final idx = _data.indexWhere((e) => e.id == id);if (idx != -1) {_data[idx] = _data[idx].copyWith(completed: !_data[idx].completed);}}Future<void> remove(String id) async {_data.removeWhere((e) => e.id == id);}
}

5.2 事件与状态

todo_event.dart

import 'package:equatable/equatable.dart';abstract class TodoEvent extends Equatable {const TodoEvent();@overrideList<Object?> get props => [];
}class TodosRequested extends TodoEvent {}
class TodoAdded extends TodoEvent {final String title;const TodoAdded(this.title);@overrideList<Object?> get props => [title];
}
class TodoToggled extends TodoEvent {final String id;const TodoToggled(this.id);@overrideList<Object?> get props => [id];
}
class TodoRemoved extends TodoEvent {final String id;const TodoRemoved(this.id);@overrideList<Object?> get props => [id];
}

todo_state.dart

import 'package:equatable/equatable.dart';
import 'todo.dart';enum TodosStatus { initial, loading, success, failure }class TodosState extends Equatable {final TodosStatus status;final List<Todo> items;final String? message;const TodosState({this.status = TodosStatus.initial,this.items = const [],this.message,});TodosState copyWith({TodosStatus? status, List<Todo>? items, String? message}) =>TodosState(status: status ?? this.status, items: items ?? this.items, message: message);@overrideList<Object?> get props => [status, items, message];
}

5.3 Bloc 实现

todo_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'todo_event.dart';
import 'todo_state.dart';
import 'todo_repository.dart';class TodoBloc extends Bloc<TodoEvent, TodosState> {final TodoRepository repo;TodoBloc({required this.repo}) : super(const TodosState()) {on<TodosRequested>(_onRequested);on<TodoAdded>(_onAdded);on<TodoToggled>(_onToggled);on<TodoRemoved>(_onRemoved);}Future<void> _onRequested(TodosRequested e, Emitter<TodosState> emit) async {emit(state.copyWith(status: TodosStatus.loading));try {final data = await repo.fetchTodos();emit(state.copyWith(status: TodosStatus.success, items: data));} catch (err) {emit(state.copyWith(status: TodosStatus.failure, message: '$err'));}}Future<void> _onAdded(TodoAdded e, Emitter<TodosState> emit) async {await repo.add(e.title);add(TodosRequested());}Future<void> _onToggled(TodoToggled e, Emitter<TodosState> emit) async {await repo.toggle(e.id);add(TodosRequested());}Future<void> _onRemoved(TodoRemoved e, Emitter<TodosState> emit) async {await repo.remove(e.id);add(TodosRequested());}
}

5.4 UI 绑定

todo_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'todo_bloc.dart';
import 'todo_state.dart';
import 'todo_event.dart';
import 'todo_repository.dart';
import 'todo.dart';class TodoPage extends StatelessWidget {const TodoPage({super.key});@overrideWidget build(BuildContext context) {return RepositoryProvider(create: (_) => TodoRepository(),child: BlocProvider(create: (ctx) => TodoBloc(repo: ctx.read<TodoRepository>())..add(TodosRequested()),child: const _TodoView(),),);}
}class _TodoView extends StatefulWidget {const _TodoView();@overrideState<_TodoView> createState() => _TodoViewState();
}class _TodoViewState extends State<_TodoView> {final _controller = TextEditingController();@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: BlocSelector<TodoBloc, TodosState, int>(selector: (state) => state.items.where((e) => !e.completed).length,builder: (_, count) => Text('Todos(未完成:$count)'),),),body: BlocConsumer<TodoBloc, TodosState>(listenWhen: (prev, curr) => prev.status != curr.status,listener: (context, state) {if (state.status == TodosStatus.failure) {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message ?? '加载失败')),);}},builder: (context, state) {if (state.status == TodosStatus.loading) {return const Center(child: CircularProgressIndicator());}if (state.status == TodosStatus.failure) {return Center(child: Text(state.message ?? '出错了'));}final items = state.items;if (items.isEmpty) {return const Center(child: Text('暂无数据'));}return ListView.separated(itemCount: items.length,separatorBuilder: (_, __) => const Divider(height: 1),itemBuilder: (_, i) {final Todo(:id, :title, :completed) = items[i];return ListTile(title: Text(title, style: TextStyle(decoration: completed ? TextDecoration.lineThrough : null)),leading: Checkbox(value: completed, onChanged: (_) => context.read<TodoBloc>().add(TodoToggled(id))),trailing: IconButton(icon: const Icon(Icons.delete), onPressed: () => context.read<TodoBloc>().add(TodoRemoved(id))),);},);},),bottomNavigationBar: SafeArea(minimum: const EdgeInsets.all(12),child: Row(children: [Expanded(child: TextField(controller: _controller,decoration: const InputDecoration(hintText: '输入待办事项'),),),const SizedBox(width: 8),ElevatedButton(onPressed: () {final text = _controller.text.trim();if (text.isNotEmpty) {context.read<TodoBloc>().add(TodoAdded(text));_controller.clear();}},child: const Text('添加'),),],),),);}
}

6. 示例三:搜索输入的防抖(RxDart)

适合:联想搜索、输入框频繁变化,减少无谓请求。

search_bloc.dart

import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rxdart/rxdart.dart';class SearchEvent extends Equatable {final String query;const SearchEvent(this.query);@overrideList<Object?> get props => [query];
}enum SearchStatus { idle, loading, success, failure }class SearchState extends Equatable {final SearchStatus status;final String query;final List<String> results;final String? message;const SearchState({this.status = SearchStatus.idle,this.query = '',this.results = const [],this.message,});SearchState copyWith({SearchStatus? status, String? query, List<String>? results, String? message}) =>SearchState(status: status ?? this.status, query: query ?? this.query, results: results ?? this.results, message: message);@overrideList<Object?> get props => [status, query, results, message];
}EventTransformer<T> debounce<T>(Duration d) {return (events, mapper) => events.debounceTime(d).switchMap(mapper);
}class SearchBloc extends Bloc<SearchEvent, SearchState> {SearchBloc() : super(const SearchState()) {on<SearchEvent>(_onQueryChanged, transformer: debounce(const Duration(milliseconds: 350)));}Future<void> _onQueryChanged(SearchEvent e, Emitter<SearchState> emit) async {if (e.query.isEmpty) {emit(state.copyWith(status: SearchStatus.idle, results: []));return;}emit(state.copyWith(status: SearchStatus.loading, query: e.query));try {// 模拟请求await Future.delayed(const Duration(milliseconds: 300));final fake = List<String>.generate(5, (i) => '${e.query}_结果_$i');emit(state.copyWith(status: SearchStatus.success, results: fake));} catch (err) {emit(state.copyWith(status: SearchStatus.failure, message: '$err'));}}
}

search_ui.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'search_bloc.dart';class SearchPage extends StatelessWidget {const SearchPage({super.key});@overrideWidget build(BuildContext context) {return BlocProvider(create: (_) => SearchBloc(),child: Scaffold(appBar: AppBar(title: const Text('搜索(防抖)')),body: Padding(padding: const EdgeInsets.all(12),child: Column(children: [TextField(decoration: const InputDecoration(hintText: '输入关键字...'),onChanged: (q) => context.read<SearchBloc>().add(SearchEvent(q)),),const SizedBox(height: 12),Expanded(child: BlocBuilder<SearchBloc, SearchState>(builder: (_, state) {switch (state.status) {case SearchStatus.loading:return const Center(child: CircularProgressIndicator());case SearchStatus.success:return ListView.builder(itemCount: state.results.length,itemBuilder: (_, i) => ListTile(title: Text(state.results[i])),);case SearchStatus.failure:return Center(child: Text(state.message ?? '出错了'));case SearchStatus.idle:default:return const Center(child: Text('开始输入进行搜索'));}},),),],),),),);}
}

不想引入 RxDart?可以改用 bloc_concurrencythrottle/droppable 实现「节流」。


7. 示例四:表单校验 + BlocListener 做副作用

login_cubit.dart

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';class LoginState extends Equatable {final String email;final String password;final bool submitting;final bool success;final String? error;const LoginState({this.email = '',this.password = '',this.submitting = false,this.success = false,this.error,});bool get valid => email.contains('@') && password.length >= 6;LoginState copyWith({String? email,String? password,bool? submitting,bool? success,String? error,}) =>LoginState(email: email ?? this.email,password: password ?? this.password,submitting: submitting ?? this.submitting,success: success ?? this.success,error: error,);@overrideList<Object?> get props => [email, password, submitting, success, error];
}class LoginCubit extends Cubit<LoginState> {LoginCubit() : super(const LoginState());void emailChanged(String v) => emit(state.copyWith(email: v, success: false, error: null));void passwordChanged(String v) => emit(state.copyWith(password: v, success: false, error: null));Future<void> submit() async {if (!state.valid) return;emit(state.copyWith(submitting: true, error: null));await Future.delayed(const Duration(milliseconds: 500)); // 模拟登录if (state.email == 'user@test.com' && state.password == '123456') {emit(state.copyWith(submitting: false, success: true));} else {emit(state.copyWith(submitting: false, success: false, error: '账号或密码错误'));}}
}

login_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'login_cubit.dart';class LoginPage extends StatelessWidget {const LoginPage({super.key});@overrideWidget build(BuildContext context) {return BlocProvider(create: (_) => LoginCubit(),child: Scaffold(appBar: AppBar(title: const Text('登录')),body: BlocListener<LoginCubit, LoginState>(listenWhen: (p, c) => p.success != c.success || p.error != c.error,listener: (context, state) {if (state.success) {ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('登录成功')));} else if (state.error != null) {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.error!)));}},child: Padding(padding: const EdgeInsets.all(16),child: BlocBuilder<LoginCubit, LoginState>(builder: (context, state) {return Column(children: [TextField(onChanged: context.read<LoginCubit>().emailChanged,decoration: const InputDecoration(labelText: '邮箱'),),TextField(onChanged: context.read<LoginCubit>().passwordChanged,obscureText: true,decoration: const InputDecoration(labelText: '密码(≥6位)'),),const SizedBox(height: 12),ElevatedButton(onPressed: state.valid && !state.submitting ? context.read<LoginCubit>().submit : null,child: state.submitting ? const CircularProgressIndicator() : const Text('登录'),)],);},),),),),);}
}

8. 持久化:HydratedBloc(如:记住计数)

counter_hydrated_cubit.dart

import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';class CounterHydratedCubit extends HydratedCubit<int> {CounterHydratedCubit() : super(0);void inc() => emit(state + 1);@overrideint? fromJson(Map<String, dynamic> json) => json['value'] as int?;@overrideMap<String, dynamic>? toJson(int state) => {'value': state};
}

main.dart(初始化)

import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';void main() async {WidgetsFlutterBinding.ensureInitialized();final storage = await HydratedStorage.build(storageDirectory: await getApplicationDocumentsDirectory());HydratedBlocOverrides.runZoned(() => runApp(const MyApp()),storage: storage,);
}

注意:iOS/Android 需要 path_provider 权限/配置正常。


9. 测试:bloc_test 编写行为用例

todo_bloc_test.dart

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'todo_bloc.dart';
import 'todo_event.dart';
import 'todo_state.dart';
import 'todo_repository.dart';void main() {group('TodoBloc', () {late TodoRepository repo;setUp(() => repo = TodoRepository());blocTest<TodoBloc, TodosState>('添加后应能拉取到数据',build: () => TodoBloc(repo: repo),act: (bloc) async {bloc.add(const TodoAdded('learn bloc'));await Future<void>.delayed(const Duration(milliseconds: 10));bloc.add(TodosRequested());},wait: const Duration(milliseconds: 400),expect: () => [// 请求列表 -> loadingisA<TodosState>().having((s) => s.status, 'status', TodosStatus.loading),// 请求成功isA<TodosState>().having((s) => s.status, 'status', TodosStatus.success).having((s) => s.items.isNotEmpty, 'has item', true),],);});
}

10. 目录组织建议(可参考)

lib/app.dartmain.dartcore/widgets/ ...utils/ ...data/models/repositories/features/todos/bloc/todo_bloc.darttodo_event.darttodo_state.dartview/todo_page.dartdata/todo_repository.dartauth/cubit/view/

11. 最佳实践与常见坑

  • 状态不可变:配合 equatable,避免手误导致比较失效。

  • 尽量薄 UI:把逻辑放到 Bloc/Cubit;UI 只读 State、发 Event。

  • 精准重建:用 BlocSelector/context.select,不要让整页频繁重建。

  • 副作用用 Listener:SnackBar/导航/弹框等用 BlocListener,不要放 Builder 里。

  • 事件风暴:输入频繁变更时防抖/节流(RxDart 或 bloc_concurrency)。

  • Bloc 间通信:A 需要 B 的状态?用 context.read<B>() 或在构造参数中传入 B 的 stream;避免循环依赖。

  • 资源释放:自己 new 的 Stream/Controller 要在 Bloc/Cubit 的 close() 里释放。

  • 错误处理:状态里留 message 字段,UI 层友好提示。

  • 可测试性:Repository 抽象 + bloc_test,单测能跑就敢重构。


12. 小结

  • 小状态用 Cubit,复杂交互用 Bloc

  • UI 专注展示,所有变化都走事件/方法 → 新状态

  • 结合 RepositoryHydratedBlocbloc_test,你的工程会更稳定、可维护。

http://www.xdnf.cn/news/18514.html

相关文章:

  • 云计算-K8s 运维:Python SDK 操作 Job/Deployment/Pod+RBAC 权限配置及自定义 Pod 调度器实战
  • 概率论基础教程第六章 随机变量的联合分布(一)
  • FastAPI + SQLAlchemy 数据库对象转字典
  • 解决coze api使用coze.workflows.runs.create运行workflow返回400,但text为空
  • SEO优化工具学习——Ahrefs进行关键词调研(包含实战)
  • 市政道路井盖缺失识别误报率↓82%!陌讯多模态融合算法实战优化与边缘部署
  • ChipCamp探索系列 -- 4. Intel CPU的十八代微架构
  • 【React Native】自定义轮盘(大转盘)组件Wheel
  • 【KO】前端面试题四
  • 今日科技热点 | 量子计算突破、AI芯片与5G加速行业变革
  • PLECS 中使用 C-Script 来模拟 NTC 热敏电阻(如 NTC3950B)
  • 【K8s】整体认识K8s之Docker篇
  • 百度面试题:赛马问题
  • 嵌入式LINUX-------------数据库
  • 循环中的阻塞风险与异步线程解法
  • 搜索体验优化:ABP vNext 的查询改写(Query Rewrite)与同义词治理
  • 前端安全之XSS和CSRF
  • 鸿蒙异步处理从入门到实战:Promise、async/await、并发池、超时重试全套攻略
  • 互联网大厂Java面试实战:核心技术栈与场景化提问解析(含Spring Boot、微服务、测试框架等)
  • 量子计算驱动的Python医疗诊断编程前沿展望(中)
  • RabbitMQ面试精讲 Day 28:Docker与Kubernetes部署实践
  • Git checkout 与 Git reset 核心区别解析(分支与版本关联逻辑)
  • 如何在 Spring Boot 中安全读取账号密码等
  • 技术演进中的开发沉思-75 Linux系列:中断和与windows中断的区分
  • 【python与生活】如何自动总结视频并输出一段总结视频?
  • 基于 FastAPI 和 OpenFeature 使用 Feature Flag 控制业务功能
  • Js逆向 拼夕夕anti_content
  • 【读代码】SQLBot:开源自然语言转SQL智能助手原理与实践
  • 怎样避免游戏检测到云手机?
  • 深入浅出:图解 glibc —— 系统与应用的沉默基石