使用 Bloc/Cubit 进行 Flutter 状态管理
Posted
技术标签:
【中文标题】使用 Bloc/Cubit 进行 Flutter 状态管理【英文标题】:Flutter State Management with Bloc/Cubit 【发布时间】:2021-12-23 01:32:05 【问题描述】:对于你们中的许多人来说,这是一个明显/愚蠢的问题,但我已经到了一个我不再有线索的地步。我很难理解 Bloc / Cubit 的状态管理。
期望:我有一个页面,其中包含所有食谱的 ListView (recipe_list) 和“添加”按钮。每当我单击 ListItem 或“添加”按钮时,我都会转到下一页(recipe_detail)。在此页面上,我可以创建一个新配方(如果之前单击“添加”按钮),更新或删除现有配方(如果之前单击 ListItem)。当我单击“保存”或“删除”按钮时,导航器会弹出并返回上一页(recipe_list)。我使用 Cubit 来管理食谱列表的状态。我的期望是单击“保存”或“删除”后 ListView 会自动更新。但我必须刷新应用才能显示更改。
main.dart
void main()
runApp(const MyApp());
class MyApp extends StatelessWidget
const MyApp(Key? key) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context)
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Recipe Demo',
home: BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository())..getAllRecipes(),
child: const RecipeList(),
)
);
recipe_list.dart
class RecipeList extends StatefulWidget
const RecipeList(Key? key) : super(key: key);
@override
_RecipeListState createState() => _RecipeListState();
class _RecipeListState extends State<RecipeList>
@override
Widget build(BuildContext context)
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0
),
color: const Color(0xFFF6F6F6),
child: Stack(
children: [
Column(
children: [
Container(
margin: const EdgeInsets.only(
top: 32.0,
bottom: 32.0
),
child: const Center(
child: Text('Recipes'),
),
),
Expanded(
child: BlocBuilder<RecipeCubit, RecipeState>(
builder: (context, state)
if (state is RecipeLoading)
return const Center(
child: CircularProgressIndicator(),
);
else if (state is RecipeError)
return const Center(
child: Icon(Icons.close),
);
else if (state is RecipeLoaded)
final recipes = state.recipes;
return ListView.builder(
itemCount: recipes.length,
itemBuilder: (context, index)
return GestureDetector(
onTap: ()
Navigator.push(context, MaterialPageRoute(
builder: (context)
return BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository()),
child: RecipeDetail(recipe: recipes[index]),
);
));
,
child: RecipeCardWidget(
title: recipes[index].title,
description: recipes[index].description,
),
);
,
);
else
return const Text('Loading recipes error');
),
),
],
),
Positioned(
bottom: 24.0,
right: 0.0,
child: FloatingActionButton(
heroTag: 'addBtn',
onPressed: ()
Navigator.push(context, MaterialPageRoute(
builder: (context)
return BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository()),
child: const RecipeDetail(recipe: null),
);
));
,
child: const Icon(Icons.add_rounded),
backgroundColor: Colors.teal,
),
)
],
),
),
),
);
recipe_detail.dart
class RecipeDetail extends StatefulWidget
final Recipe? recipe;
const RecipeDetail(Key? key, required this.recipe) : super(key: key);
@override
_RecipeDetailState createState() => _RecipeDetailState();
class _RecipeDetailState extends State<RecipeDetail>
final RecipeRepository recipeRepository = RecipeRepository();
final int _recipeId = 0;
late String _recipeTitle = '';
late String _recipeDescription = '';
final recipeTitleController = TextEditingController();
final recipeDescriptionController = TextEditingController();
late FocusNode _titleFocus;
late FocusNode _descriptionFocus;
bool _buttonVisible = false;
@override
void initState()
if (widget.recipe != null)
_recipeTitle = widget.recipe!.title;
_recipeDescription = widget.recipe!.description;
_buttonVisible = true;
_titleFocus = FocusNode();
_descriptionFocus = FocusNode();
super.initState();
@override
void dispose()
recipeTitleController.dispose();
recipeDescriptionController.dispose();
_titleFocus.dispose();
_descriptionFocus.dispose();
super.dispose();
@override
Widget build(BuildContext context)
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0
),
color: const Color(0xFFF6F6F6),
child: Stack(
children: [
Column(
children: [
Align(
alignment: Alignment.topLeft,
child: InkWell(
child: IconButton(
highlightColor: Colors.transparent,
color: Colors.black54,
onPressed: ()
Navigator.pop(context);
,
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
),
),
TextField(
focusNode: _titleFocus,
controller: recipeTitleController..text = _recipeTitle,
decoration: const InputDecoration(
hintText: 'Enter recipe title',
border: InputBorder.none
),
style: const TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
),
onSubmitted: (value) => _descriptionFocus.requestFocus(),
),
TextField(
focusNode: _descriptionFocus,
controller: recipeDescriptionController..text = _recipeDescription,
decoration: const InputDecoration(
hintText: 'Enter recipe description',
border: InputBorder.none
),
),
],
),
Positioned(
bottom: 24.0,
left: 0.0,
child: FloatingActionButton(
heroTag: 'saveBtn',
onPressed: ()
if (widget.recipe == null)
Recipe _newRecipe = Recipe(
_recipeId,
recipeTitleController.text,
recipeDescriptionController.text
);
context.read<RecipeCubit>().createRecipe(_newRecipe);
//recipeRepository.createRecipe(_newRecipe);
Navigator.pop(context);
else
Recipe _newRecipe = Recipe(
widget.recipe!.id,
recipeTitleController.text,
recipeDescriptionController.text
);
context.read<RecipeCubit>().updateRecipe(_newRecipe);
//recipeRepository.updateRecipe(_newRecipe);
Navigator.pop(context);
,
child: const Icon(Icons.save_outlined),
backgroundColor: Colors.amberAccent,
),
),
Positioned(
bottom: 24.0,
right: 0.0,
child: Visibility(
visible: _buttonVisible,
child: FloatingActionButton(
heroTag: 'deleteBtn',
onPressed: ()
context.read<RecipeCubit>().deleteRecipe(widget.recipe!.id!);
//recipeRepository.deleteRecipe(widget.recipe!.id!);
Navigator.pop(context);
,
child: const Icon(Icons.delete_outline_rounded),
backgroundColor: Colors.redAccent,
),
),
),
],
),
),
),
);
recipe_state.dart
part of 'recipe_cubit.dart';
abstract class RecipeState extends Equatable
const RecipeState();
class RecipeInitial extends RecipeState
@override
List<Object> get props => [];
class RecipeLoading extends RecipeState
@override
List<Object> get props => [];
class RecipeLoaded extends RecipeState
final List<Recipe> recipes;
const RecipeLoaded(this.recipes);
@override
List<Object> get props => [recipes];
class RecipeError extends RecipeState
final String message;
const RecipeError(this.message);
@override
List<Object> get props => [message];
recipe_cubit.dart
part 'recipe_state.dart';
class RecipeCubit extends Cubit<RecipeState>
final RecipeRepository recipeRepository;
RecipeCubit(this.recipeRepository) : super(RecipeInitial())
getAllRecipes();
void getAllRecipes() async
try
emit(RecipeLoading());
final recipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(recipes));
catch (e)
emit(const RecipeError('Error'));
void createRecipe(Recipe recipe) async
await recipeRepository.createRecipe(recipe);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
void updateRecipe(Recipe recipe) async
await recipeRepository.updateRecipe(recipe);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
void deleteRecipe(int id) async
await recipeRepository.deleteRecipe(id);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
【问题讨论】:
【参考方案1】:当您导航到 RecipeDetail
页面时,您似乎正在创建另一个 BlocProvider
。当你推送新的MaterialPageRoute
时,这个新页面会另外包裹在新的RecipeCubit
中。然后,当您调用context.read<RecipeCubit>()
时,您将引用该提供程序(因为这是小部件树中最接近的BlocProvider
)。您的RecipeList
无法对这些更改做出反应,因为它的BlocBuilder
正在寻找在小部件树(MyApp
中的那个)上方声明的BlocProvider
。
除此之外,当您关闭 RecipeDetail
页面时,新创建的提供程序无论如何都会从小部件树中删除,因为它在刚刚被推离屏幕的 MaterialPageRoute
中声明。
尝试删除额外的BlocProvider
(RecipeList
中的那个,在RecipeCardWidget
的OnTap
函数中):
onTap: ()
Navigator.push(context, MaterialPageRoute(
builder: (context)
return BlocProvider<RecipeCubit>( // remove this BlocProvider
create: (context) => RecipeCubit(RecipeRepository()),
child: RecipeDetail(recipe: recipes[index]),
);
));
,
【讨论】:
感谢您对这个问题的快速回答。我删除了提到的 BlocProvider。现在,在提交文本字段(在 recipe_detail 页面上)后,单击“添加”按钮仍会创建一个新配方。但仍然没有更新的 ListView 没有刷新。当我现在在 recipe_list 页面上单击“保存”或“删除”时,我收到此错误:手势捕获的异常处理手势时引发了以下 ProviderNotFoundException:错误:在此 RecipeDetail Widget 上方找不到正确的 Provider以上是关于使用 Bloc/Cubit 进行 Flutter 状态管理的主要内容,如果未能解决你的问题,请参考以下文章
Flutter BLoC/Cubit STATE 类最佳实践
Flutter BLoC 库:将 TextEditingController 对象保存在哪里:在 State、BLoC / Cubit 类中还是在小部件中?
具有多个firebase请求的flutter bloc cubit最佳实践
使用相同 Bloc/Cubit 的多个 BlocBuilder,每个 BlocBuilder 用于不同的事件