Home > Software design >  Flutter State Management with Bloc/Cubit
Flutter State Management with Bloc/Cubit

Time:11-11

for many of you this is an obvious / stupid question, but I've come to a point where I don't have a clue anymore. I have real difficulties understanding State Management with Bloc / Cubit.

Expectation: I have a page with a ListView (recipe_list) of all recipes and an 'add' button. Whenever I click on a ListItem or the 'add' button I go to the next page (recipe_detail). On this page I can create a new recipe (if clicked the 'add' button before), update or delete the existing recipe (if clicked on ListItem before). When I click the 'save' or 'delete' button the Navigator pops and I go back to the previous page (recipe_list). I used Cubit to manage the state of the recipe list. My expectation is that the ListView updates automatically after I clicked 'save' or 'delete'. But I have to refresh the App to display the changes.

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

CodePudding user response:

It looks like you're creating another BlocProvider when you're navigating to RecipeDetail page. When you're pushing new MaterialPageRoute, this new page gets additionally wrapped in new RecipeCubit. Then, when you're calling context.read<RecipeCubit>(), you're referencing that provider (as this is closest BlocProvider in the widget tree). Your RecipeList can't react to those changes because it's BlocBuilder is looking for a BlocProvider declared above it in the widget tree (the one in MyApp). Besides that, newly created provider gets removed from the widget tree anyway when you're closing RecipeDetail page as it is declared in the MaterialPageRoute which has just been pushed off the screen.

Try to remove the additional BlocProvider (the one in RecipeList, in OnTap function of RecipeCardWidget):

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