使用 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&lt;RecipeCubit&gt;() 时,您将引用该提供程序(因为这是小部件树中最接近的BlocProvider)。您的RecipeList 无法对这些更改做出反应,因为它的BlocBuilder 正在寻找在小部件树(MyApp 中的那个)上方声明的BlocProvider。 除此之外,当您关闭 RecipeDetail 页面时,新创建的提供程序无论如何都会从小部件树中删除,因为它在刚刚被推离屏幕的 MaterialPageRoute 中声明。

尝试删除额外的BlocProviderRecipeList中的那个,在RecipeCardWidgetOnTap函数中):

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。 .... 之前遇到此错误,这就是我添加 BlocProvider 的原因 哦,你能把你的第一个 BlocProvider 移到小部件树中 MaterialApp 的上方吗?这样你就可以在你的应用程序中全局访问它。我忘了提这个。 好的,错误消失了。现在:当我单击 ListItem 并更新/删除食谱时,它会自动更新 ListView。但是,当我单击“添加”按钮创建新配方时,它仍然仅在刷新后更新 ListView。但到目前为止,非常感谢你帮了我很多。编辑:刚刚从“添加”按钮中删除了 BlocProvider 。现在一切正常。谢谢!

以上是关于使用 Bloc/Cubit 进行 Flutter 状态管理的主要内容,如果未能解决你的问题,请参考以下文章

Flutter BLoC/Cubit STATE 类最佳实践

Flutter BLoC 库:将 TextEditingController 对象保存在哪里:在 State、BLoC / Cubit 类中还是在小部件中?

具有多个firebase请求的flutter bloc cubit最佳实践

使用相同 Bloc/Cubit 的多个 BlocBuilder,每个 BlocBuilder 用于不同的事件

我应该先启动 Cubit,然后再将代码重构为 Bloc 吗?

当网络连接恢复时,使用 Bloc /Cubit 颤振不会发生自动刷新?