Bloc状态管理
为什么我的Flutter应用越来越难维护?
记得刚接触Flutter时,觉得setState简直太方便了。但随着项目规模扩大,问题也逐渐暴漏出来:
问题1:状态分散难以管理
// 不推荐
class ProductPage extends StatefulWidget {
@override
_ProductPageState createState() => _ProductPageState();
}
class _ProductPageState extends State<ProductPage> {
Product? _product;
bool _isLoading = false;
String? _errorMessage;
bool _isFavorite = false;
bool _isInCart = false;
// 各种异步方法混在一起
Future<void> _loadProduct() async {
setState(() => _isLoading = true);
try {
_product = await repository.getProduct();
_isFavorite = await repository.checkFavorite();
_isInCart = await repository.checkCart();
} catch (e) {
_errorMessage = e.toString();
} finally {
setState(() => _isLoading = false);
}
}
}
问题2:跨组件状态共享困难
// 用户登录后,需要同步更新多个组件
class Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 如何获取用户状态?
}
}
class ProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 如何获取用户状态?
}
}
class Sidebar extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 如何获取用户状态?
}
}
问题3:业务逻辑与UI耦合
// 业务逻辑分散在UI层,难以测试和维护
void _onAddToCart() async {
// 验证登录状态
// 检查库存
// 调用API
// 更新本地状态
// 显示结果提示
// 所有这些逻辑都混在一起!
}
面对这些问题,进行了多种状态管理方案尝试,最终发现Bloc提供了最清晰的架构和最佳的可维护性。
一、Bloc核心原理:单向数据流
1.1 Bloc
Bloc的核心思想可以用一句话概括:UI只关心显示什么,不关心为什么这样显示。
1.2 Bloc架构图
先通过一个完整的架构图来理解Bloc的各个组成部分:
graph TB
subgraph "UI Layer (表示层)"
A[Widgets] --> B[发送 Events]
C[BlocBuilder] --> D[重建 UI]
E[BlocListener] --> F[处理副作用]
end
subgraph "Bloc Layer (业务逻辑层)"
B --> G[Bloc]
G --> H[States]
H --> C
H --> E
subgraph "Bloc内部结构"
G --> I[Event Handler]
I --> J[业务逻辑]
J --> K[State Emitter]
K --> H
end
end
subgraph "Data Layer (数据层)"
J --> L[Repository]
L --> M[Local Data]
L --> N[Remote Data]
M --> O[SQLite/SharedPrefs]
N --> P[API/Network]
end
style G fill:#e1f5fe
style J fill:#f3e5f5
style L fill:#e8f5e8
架构分层详解:
| 层级 |
职责 |
对应代码 |
| UI层 |
显示界面、用户交互 |
Widget、BlocBuilder、BlocListener |
| Bloc层 |
处理业务逻辑、状态管理 |
Bloc、Cubit、Event、State |
| 数据层 |
数据获取和持久化 |
Repository、DataSource、Model |
1.3 数据流向原理
Bloc采用严格的单向数据流,这是它可预测性的关键:
sequenceDiagram
participant U as UI Widget
participant B as Bloc
participant R as Repository
participant S as State
U->>B: 发送 Event
Note over B: 处理业务逻辑
B->>R: 调用数据方法
R->>B: 返回数据结果
B->>S: 发射新 State
S->>U: 触发重建
Note over U: 根据State显示界面
数据流特点:
-
单向性:数据只能沿一个方向流动
-
可预测:相同的Event总是产生相同的State变化
-
可追踪:可以清晰追踪状态变化的完整路径
二、Bloc核心概念
2.1 Event(事件)
Event代表从UI层发送到Bloc的"指令",它描述了"要做什么",但不关心"怎么做"。
Event设计原则
// 好的Event设计
abstract class ProductEvent {}
// 具体的事件 - 使用命名构造函数
class ProductEvent {
const ProductEvent._();
factory ProductEvent.load(String productId) = ProductLoadEvent;
factory ProductEvent.addToCart(String productId, int quantity) = ProductAddToCartEvent;
factory ProductEvent.toggleFavorite(String productId) = ProductToggleFavoriteEvent;
}
// 具体的事件类
class ProductLoadEvent extends ProductEvent {
final String productId;
const ProductLoadEvent(this.productId);
}
class ProductAddToCartEvent extends ProductEvent {
final String productId;
final int quantity;
const ProductAddToCartEvent(this.productId, this.quantity);
}
class ProductToggleFavoriteEvent extends ProductEvent {
final String productId;
const ProductToggleFavoriteEvent(this.productId);
}
Event分类策略
在实际项目中,我会这样组织Event:
events/
├── product_event.dart
├── cart_event.dart
├── auth_event.dart
└── order_event.dart
2.2 State(状态)
State代表应用在某个时刻的完整状况,UI完全由State驱动。
State设计模式
// 状态基类
sealed class ProductState {
const ProductState();
}
// 具体的状态类
class ProductInitialState extends ProductState {
const ProductInitialState();
}
class ProductLoadingState extends ProductState {
const ProductLoadingState();
}
class ProductLoadedState extends ProductState {
final Product product;
final bool isInCart;
final bool isFavorite;
const ProductLoadedState({
required this.product,
required this.isInCart,
required this.isFavorite,
});
// 复制方法 - 用于不可变更新
ProductLoadedState copyWith({
Product? product,
bool? isInCart,
bool? isFavorite,
}) {
return ProductLoadedState(
product: product ?? this.product,
isInCart: isInCart ?? this.isInCart,
isFavorite: isFavorite ?? this.isFavorite,
);
}
}
class ProductErrorState extends ProductState {
final String message;
final Object? error;
const ProductErrorState(this.message, [this.error]);
}
State状态机模型
理解State之间的关系很重要,它们形成一个状态机:
stateDiagram-v2
[*] --> Initial: 初始化
Initial --> Loading: 开始加载
Loading --> Loaded: 加载成功
Loading --> Error: 加载失败
Loaded --> Loading: 重新加载
Loaded --> Updating: 开始更新
Updating --> Loaded: 更新成功
Updating --> Error: 更新失败
Error --> Loading: 重试
Error --> [*]: 重置
State设计要点:
- 包含UI需要的所有数据
- 使用final和const
- 便于调试和持久化
- 清晰区分加载、成功、错误等状态
2.3 Bloc
Bloc是连接Event和State的桥梁,包含所有的业务逻辑。
Bloc核心结构
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository repository;
ProductBloc({required this.repository}) : super(const ProductInitialState()) {
// 注册事件处理器
on<ProductLoadEvent>(_onLoad);
on<ProductAddToCartEvent>(_onAddToCart);
on<ProductToggleFavoriteEvent>(_onToggleFavorite);
}
// 事件处理方法的详细实现
Future<void> _onLoad(
ProductLoadEvent event,
Emitter<ProductState> emit,
) async {
try {
emit(const ProductLoadingState());
// 并行获取多个数据
final results = await Future.wait([
repository.getProduct(event.productId),
repository.isInCart(event.productId),
repository.isFavorite(event.productId),
]);
final product = results[0] as Product;
final isInCart = results[1] as bool;
final isFavorite = results[2] as bool;
emit(ProductLoadedState(
product: product,
isInCart: isInCart,
isFavorite: isFavorite,
));
} catch (error, stackTrace) {
// 详细的错误处理
emit(ProductErrorState(
'加载商品失败',
error,
));
addError(error, stackTrace);
}
}
Future<void> _onAddToCart(
ProductAddToCartEvent event,
Emitter<ProductState> emit,
) async {
final currentState = state;
// 状态保护
if (currentState is! ProductLoadedState) return;
try {
emit(currentState.copyWith(isInCart: true));
await repository.addToCart(event.productId, event.quantity);
} catch (error) {
emit(currentState.copyWith(isInCart: false));
rethrow;
}
}
Future<void> _onToggleFavorite(
ProductToggleFavoriteEvent event,
Emitter<ProductState> emit,
) async {
final currentState = state;
if (currentState is! ProductLoadedState) return;
final newFavoriteStatus = !currentState.isFavorite;
try {
emit(currentState.copyWith(isFavorite: newFavoriteStatus));
await repository.toggleFavorite(event.productId);
} catch (error) {
emit(currentState.copyWith(isFavorite: !newFavoriteStatus));
rethrow;
}
}
}
Bloc内部工作原理
下面我们深入了解Bloc如何处理事件和状态:
graph TB
A[Event输入] --> B[Event队列]
B --> C[事件循环]
subgraph "事件处理流程"
C --> D{查找事件处理器}
D --> E[找到处理器]
E --> F[执行业务逻辑]
F --> G[状态发射器]
G --> H[状态输出]
D --> I[无处理器]
I --> J[忽略事件]
end
H --> K[State流]
K --> L[UI更新]
style F fill:#f3e5f5
style G fill:#e1f5fe
三、BlocBuilder与BlocListener
3.1 BlocBuilder
BlocBuilder监听状态变化并重建对应的UI部分。
基本使用模式
class ProductPage extends StatelessWidget {
final String productId;
const ProductPage({super.key, required this.productId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ProductBloc(
repository: context.read<ProductRepository>(),
)..add(ProductEvent.load(productId)),
child: Scaffold(
appBar: AppBar(title: const Text('商品详情')),
body: const _ProductContent(),
),
);
}
}
class _ProductContent extends StatelessWidget {
const _ProductContent();
@override
Widget build(BuildContext context) {
return BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return switch (state) {
ProductInitialState() => _buildInitialView(),
ProductLoadingState() => _buildLoadingView(),
ProductLoadedState(
product: final product,
isInCart: final isInCart,
isFavorite: final isFavorite,
) => _buildProductView(product, isInCart, isFavorite, context),
ProductErrorState(message: final message) => _buildErrorView(message),
};
},
);
}
Widget _buildProductView(
Product product,
bool isInCart,
bool isFavorite,
BuildContext context,
) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 商品图片
AspectRatio(
aspectRatio: 1,
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
),
),
const SizedBox(height: 16),
// 商品信息
Text(
product.name,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
// 价格
Text(
'¥${product.price}',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.red,
),
),
const SizedBox(height: 16),
// 描述
Text(
product.description,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
// 操作按钮区域
_buildActionButtons(product, isInCart, isFavorite, context),
],
),
);
}
Widget _buildActionButtons(
Product product,
bool isInCart,
bool isFavorite,
BuildContext context,
) {
return Row(
children: [
// 收藏按钮
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : Colors.grey,
),
onPressed: () {
context.read<ProductBloc>().add(
ProductEvent.toggleFavorite(product.id),
);
},
),
const Spacer(),
// 购物车按钮
FilledButton.icon(
icon: const Icon(Icons.shopping_cart),
label: Text(isInCart ? '已加购' : '加入购物车'),
onPressed: isInCart ? null : () {
context.read<ProductBloc>().add(
ProductEvent.addToCart(product.id, 1),
);
},
),
],
);
}
Widget _buildLoadingView() {
return const Center(
child: CircularProgressIndicator(),
);
}
Widget _buildErrorView(String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('加载失败: $message'),
],
),
);
}
Widget _buildInitialView() {
return const Center(
child: Text('准备加载商品信息...'),
);
}
}
BlocBuilder性能优化
// 不推荐 - 整个页面重建
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text('商品')), // 每次重建
body: _buildBody(state), // 每次重建
);
},
)
// 推荐 - 局部重建
Scaffold(
appBar: const AppBar(title: Text('商品')), // 不重建
body: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return _buildBody(state); // 只有这部分重建
},
),
)
Column(
children: [
const Header(),
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return ProductImage(state.product.imageUrl);
},
),
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return ProductInfo(state.product);
},
),
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return ActionButtons(state);
},
),
],
)
3.2 BlocListener
BlocListener用于响应状态变化执行一次性操作,如导航、显示对话框等。
处理模式
class _ProductContent extends StatelessWidget {
const _ProductContent();
@override
Widget build(BuildContext context) {
return BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
// 处理错误状态
if (state is ProductErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
// 处理成功加入购物车
if (state is ProductLoadedState && state.isInCart) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('成功加入购物车!'),
backgroundColor: Colors.green,
),
);
}
// 处理特定业务逻辑
_handleSpecialStates(state, context);
},
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
// UI构建逻辑
return _buildContent(state);
},
),
);
}
void _handleSpecialStates(ProductState state, BuildContext context) {
switch (state) {
case ProductLoadedState(:final product) when product.stock < 10:
// 库存不足提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} 库存紧张!'),
backgroundColor: Colors.orange,
),
);
case ProductLoadedState(:final product) when product.isNew:
// 新品提示
_showNewProductDialog(context, product);
default:
break;
}
}
void _showNewProductDialog(BuildContext context, Product product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('新品上架!'),
content: Text('${product.name} 是刚刚上架的新品!'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('知道了'),
),
],
),
);
}
}
3.3 BlocConsumer
当需要同时使用Builder和Listener时,BlocConsumer提供了更简洁的写法。
BlocConsumer<ProductBloc, ProductState>(
listener: (context, state) {
// 处理副作用
if (state is ProductErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
builder: (context, state) {
// 构建UI
return switch (state) {
ProductLoadedState(:final product) => ProductDetails(product: product),
_ => const LoadingIndicator(),
};
},
)
四、 多Bloc协作模式
class AddToCartButton extends StatelessWidget {
final String productId;
const AddToCartButton({super.key, required this.productId});
@override
Widget build(BuildContext context) {
return BlocListener<CartBloc, CartState>(
listener: (context, state) {
// 监听购物车状态变化
if (state is CartErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, productState) {
final isInCart = switch (productState) {
ProductLoadedState(:final isInCart) => isInCart,
_ => false,
};
return FilledButton(
onPressed: isInCart ? null : () {
// 同时更新商品状态和购物车状态
context.read<ProductBloc>().add(
ProductEvent.addToCart(productId, 1),
);
context.read<CartBloc>().add(
CartAddItemEvent(productId, 1),
);
},
child: Text(isInCart ? '已加入购物车' : '加入购物车'),
);
},
),
);
}
}
4.1 Bloc间通信模式
方式1:直接事件传递
// 在商品Bloc中监听购物车事件
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final CartBloc cartBloc;
ProductBloc({required this.cartBloc}) : super(const ProductInitialState()) {
// 监听购物车变化
cartBloc.stream.listen((cartState) {
if (cartState is CartLoadedState && state is ProductLoadedState) {
// 同步购物车状态
final isInCart = cartState.items.any(
(item) => item.productId == (state as ProductLoadedState).product.id,
);
add(ProductSyncCartEvent(isInCart));
}
});
}
}
方式2:通过Repository共享状态
class CartRepository {
final StreamController<Cart> _cartController = StreamController.broadcast();
Stream<Cart> get cartStream => _cartController.stream;
Future<void> addItem(String productId, int quantity) async {
// 添加商品逻辑...
_cartController.add(updatedCart);
}
}
// 多个Bloc监听同一个Repository
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final CartRepository cartRepository;
StreamSubscription? _cartSubscription;
ProductBloc({required this.cartRepository}) : super(const ProductInitialState()) {
// 监听购物车变化
_cartSubscription = cartRepository.cartStream.listen((cart) {
if (state is ProductLoadedState) {
final isInCart = cart.items.any(
(item) => item.productId == (state as ProductLoadedState).product.id,
);
add(ProductSyncCartEvent(isInCart));
}
});
}
@override
Future<void> close() {
_cartSubscription?.cancel();
return super.close();
}
}
4.2 高级模式:Bloc转换器和并发控制
class ProductBloc extends Bloc<ProductEvent, ProductState> {
ProductBloc() : super(const ProductInitialState()) {
on<ProductEvent>(
_onEvent,
// 转换器配置
transformer: (events, mapper) {
return events
.debounceTime(const Duration(milliseconds: 300)) // 防抖
.asyncExpand(mapper); // 并发控制
},
);
}
Future<void> _onEvent(
ProductEvent event,
Emitter<ProductState> emit,
) async {
// 事件处理逻辑
}
}
五、项目结构
5.1 完整的项目结构
lib/
├── src/
│ ├── app/ # 应用层
│ │ ├── app.dart
│ │ └── routes/
│ ├── features/ # 功能模块
│ │ ├── product/
│ │ │ ├── bloc/ # Bloc相关
│ │ │ │ ├── product_bloc.dart
│ │ │ │ ├── product_event.dart
│ │ │ │ ├── product_state.dart
│ │ │ │ └── product_bloc.freezed.dart
│ │ │ ├── views/ # 页面
│ │ │ ├── widgets/ # 组件
│ │ │ └── models/ # 模型
│ │ ├── cart/
│ │ └── auth/
│ ├── core/ # 核心层
│ │ ├── bloc/ # Bloc基础设施
│ │ │ ├── app_bloc_observer.dart
│ │ │ └── bloc_providers.dart
│ │ ├── data/ # 数据层
│ │ │ ├── repositories/
│ │ │ ├── datasources/
│ │ │ └── models/
│ │ ├── di/ # 依赖注入
│ │ │ └── service_locator.dart
│ │ └── utils/ # 工具类
│ └── shared/ # 共享资源
│ ├── widgets/
│ ├── themes/
│ └── constants/
└── main.dart
5.2 依赖注入配置
// service_locator.dart
final getIt = GetIt.instance;
void setupDependencies() {
// 数据层
getIt.registerLazySingleton<ProductRepository>(
() => ProductRepositoryImpl(
localDataSource: getIt(),
remoteDataSource: getIt(),
),
);
getIt.registerLazySingleton<CartRepository>(
() => CartRepositoryImpl(
localDataSource: getIt(),
remoteDataSource: getIt(),
),
);
// Bloc层 - 使用工厂,因为可能有多个实例
getIt.registerFactoryParam<ProductBloc, String, void>(
(productId, _) => ProductBloc(
repository: getIt<ProductRepository>(),
productId: productId,
),
);
// 购物车Bloc使用单例,因为全局只有一个购物车
getIt.registerLazySingleton<CartBloc>(
() => CartBloc(repository: getIt<CartRepository>()),
);
// 认证Bloc使用单例
getIt.registerLazySingleton<AuthBloc>(
() => AuthBloc(repository: getIt<AuthRepository>()),
);
}
5.3 应用启动配置
void main() {
// 确保Widget绑定初始化
WidgetsFlutterBinding.ensureInitialized();
// 设置依赖注入
setupDependencies();
// 设置Bloc观察者
Bloc.observer = AppBlocObserver();
// 错误处理
BlocOverrides.runZoned(
() => runApp(const MyApp()),
blocObserver: AppBlocObserver(),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
// 全局Bloc
BlocProvider(create: (context) => getIt<AuthBloc>()),
BlocProvider(create: (context) => getIt<CartBloc>()),
],
child: MaterialApp(
title: '电商应用',
theme: AppTheme.light,
darkTheme: AppTheme.dark,
home: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return switch (state) {
AuthAuthenticated() => const HomePage(),
_ => const LoginPage(),
};
},
),
routes: AppRoutes.routes,
),
);
}
}
六、单元测试
6.1 Bloc单元测试
void main() {
group('ProductBloc测试', () {
late ProductBloc productBloc;
late MockProductRepository mockRepository;
setUp(() {
mockRepository = MockProductRepository();
productBloc = ProductBloc(repository: mockRepository);
});
tearDown(() {
productBloc.close();
});
test('初始状态正确', () {
expect(productBloc.state, equals(const ProductInitialState()));
});
test('加载商品成功流程', () async {
// 准备
const product = Product(
id: '1',
name: '测试商品',
price: 100,
imageUrl: 'test.jpg',
description: '测试描述',
);
when(mockRepository.getProduct('1'))
.thenAnswer((_) async => product);
when(mockRepository.isInCart('1'))
.thenAnswer((_) async => false);
when(mockRepository.isFavorite('1'))
.thenAnswer((_) async => true);
// 期望的状态序列
final expectedStates = [
const ProductInitialState(),
const ProductLoadingState(),
const ProductLoadedState(
product: product,
isInCart: false,
isFavorite: true,
),
];
// 执行并验证
expectLater(
productBloc.stream,
emitsInOrder(expectedStates),
);
productBloc.add(const ProductEvent.load('1'));
});
test('添加到购物车成功', () async {
// 先加载商品
const product = Product(id: '1', name: '测试商品', price: 100);
when(mockRepository.getProduct('1')).thenAnswer((_) async => product);
when(mockRepository.isInCart('1')).thenAnswer((_) async => false);
when(mockRepository.isFavorite('1')).thenAnswer((_) async => false);
productBloc.add(const ProductEvent.load('1'));
await pumpEventQueue();
// 准备添加到购物车
when(mockRepository.addToCart('1', 1))
.thenAnswer((_) async {});
// 执行添加到购物车
productBloc.add(const ProductEvent.addToCart('1', 1));
// 验证状态变化
await expectLater(
productBloc.stream,
emitsThrough(
const ProductLoadedState(
product: product,
isInCart: true, // 应该变为true
isFavorite: false,
),
),
);
});
});
}
6.2 Widget测试
void main() {
group('ProductPage Widget测试', () {
testWidgets('显示加载状态', (WidgetTester tester) async {
// 创建测试Bloc
final productBloc = MockProductBloc();
when(productBloc.state).thenReturn(const ProductLoadingState());
await tester.pumpWidget(
MaterialApp(
home: BlocProvider.value(
value: productBloc,
child: const ProductPage(productId: '1'),
),
),
);
// 验证显示加载指示器
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('显示商品信息', (WidgetTester tester) async {
final productBloc = MockProductBloc();
const product = Product(
id: '1',
name: '测试商品',
price: 100,
imageUrl: 'test.jpg',
description: '测试描述',
);
when(productBloc.state).thenReturn(
const ProductLoadedState(
product: product,
isInCart: false,
isFavorite: false,
),
);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider.value(
value: productBloc,
child: const ProductPage(productId: '1'),
),
),
);
// 验证商品信息显示
expect(find.text('测试商品'), findsOneWidget);
expect(find.text('¥100'), findsOneWidget);
expect(find.text('测试描述'), findsOneWidget);
});
});
}
结语
通过以上学习,我们系统掌握了Bloc状态管理的完整体系:架构思想、三大核心概念、核心组件、高级特性,如果觉得这篇文章对你有帮助,别忘了一键三连(点赞、关注、收藏)~~~**
在实际开发中遇到任何Bloc相关问题,欢迎在评论区留言。
版权声明:本文内容基于多个商业项目实战经验总结,欢迎分享交流,但请注明出处。