Bloc/Cubit 的 Flutter 状态管理

Flutter State Management with Bloc/Cubit

对于你们中的许多人来说,这是一个显而易见的/愚蠢的问题,但我已经到了我不再有任何线索的地步。我很难理解 Bloc / Cubit 的状态管理。

期望:我有一个包含所有食谱的 ListView (recipe_list) 和一个 'add' 按钮的页面。每当我单击 ListItem 或 'add' 按钮时,我都会转到下一页 (recipe_detail)。在此页面上,我可以创建一个新配方(如果之前单击了 'add' 按钮),更新或删除现有配方(如果之前单击了 ListItem)。当我单击 'save' 或 'delete' 按钮时,导航器弹出,我返回到上一页 (recipe_list)。我使用 Cubit 来管理食谱列表的状态。我希望 ListView 在我单击 'save' 或 'delete' 后自动更新。但是我必须刷新应用程序才能显示更改。

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));
  }
}

当您导航到 RecipeDetail 页面时,您似乎正在创建另一个 BlocProvider。当您推送新的 MaterialPageRoute 时,这个新页面会另外包含在新的 RecipeCubit 中。然后,当您调用 context.read<RecipeCubit>() 时,您将引用该提供程序(因为它在小部件树中最接近 BlocProvider)。您的 RecipeList 无法对这些更改做出反应,因为它 BlocBuilder 正在寻找在窗口小部件树(MyApp 中的那个)上方声明的 BlocProvider。 除此之外,当您关闭 RecipeDetail 页面时,新创建的提供程序无论如何都会从小部件树中删除,因为它在刚刚被推出屏幕的 MaterialPageRoute 中声明。

尝试删除额外的 BlocProviderRecipeListRecipeCardWidgetOnTap 函数中的那个):

onTap: () {
  Navigator.push(context, MaterialPageRoute(
      builder: (context) {
        return BlocProvider<RecipeCubit>(  // remove this BlocProvider
          create: (context) => RecipeCubit(RecipeRepository()),
          child: RecipeDetail(recipe: recipes[index]),
        );
      }
  ));
},