Flutter BLoC: How to add a new event after Navigator.push without wrapping MaterialApp with BlocProvider

Issue

So I have screen ExploreScreen which initiates my BLoC FilteredRecipesBloc.
Inside this screen, there is a button that navigates to a new screen FilterScreen.
From the FilterScreen, I want to add a new event which affects both screens. The problem now is that I’m getting this error message (onError Bad state: Cannot add new events after calling close). Is this possible without wrapping MaterialApp with a BlocProvider? I just want local bloc access to two screens.

ExploreScreen:

class ExploreScreen extends StatefulWidget {
  @override
  _ExploreScreenState createState() => _ExploreScreenState();
}

class _ExploreScreenState extends State<ExploreScreen> {
  FilteredRecipesBloc _filteredRecipesBloc;

  @override
  void initState() {
    _filteredRecipesBloc = FilteredRecipesBloc(
        recipeList:
            (BlocProvider.of<RecipesBloc>(context).state as RecipesLoaded)
                .recipeList);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Explore"),
      ),
      body: BlocProvider(
        create: (context) => _filteredRecipesBloc,
        child: BlocBuilder<FilteredRecipesBloc, FilteredRecipesState>(
            builder: (context, state) {
          if (state is FilteredRecipeEmpty) {
            return CategoriesScreen();
          }
          if (state is FilteredRecipesLoading) {
            return Column(
              children: <Widget>[
                CircularProgressIndicator(),
                IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    _filteredRecipesBloc.add(UpdateRecipesFilter(
                        ingredients: ["Curry"], maxCookTime: 30));
                  },
                ),
              ],
            );
          }
          if (state is FilteredRecipeLoaded) {
            return ListView.builder(
                itemCount: state.recipeList.length,
                itemBuilder: (_, int index) {
                  return ImageRecipeContainer(recipe: state.recipeList[index]);
                });
          }
          return Container();
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _navigateToFilterScreen,
        child: Icon(EvaIcons.funnelOutline),
        heroTag: "fafdsf",
      ),
    );
  }

  void _navigateToFilterScreen() {
    Navigator.of(context)
        .push(MaterialPageRoute<FilterScreen>(builder: (context) {
      return BlocProvider.value(
        value: _filteredRecipesBloc,
        child: FilterScreen(_filteredRecipesBloc),
      );
    }));
  }

  @override
  void dispose() {
    _filteredRecipesBloc.close();
    super.dispose();
  }
}

Filter Screen:

class FilterScreen extends StatefulWidget {
  final FilteredRecipesBloc filteredRecipesBloc;

  FilterScreen(this.filteredRecipesBloc);
  @override
  _FilterScreenState createState() => _FilterScreenState();
}

class _FilterScreenState extends State<FilterScreen> {
  Map<String, bool> _selectedCategories = {};
  Map<String, bool> _selectedIngredients = {};

  @override
  void initState() {
    _initIngredientList();
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Filter"),),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(12.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text("Category",style: TextStyle(fontSize: 20,fontWeight: FontWeight.bold),),
              ChoiceChip(
                  label: Text("Vegan"),
                selected: _selectedCategories["vegan"] == true,
                onSelected: (isActive){
                    setState(() {
                      _selectedCategories["vegan"] = isActive;
                    });
                },
              ),
              Text("Ingredients"),
              ShowMoreChoiceChips(children: _buildIngredientChoiceChips()),
              RaisedButton(onPressed: _updateFilter),


            ],
          ),
        ),
      ),
    );
  }

  void _initIngredientList(){
    List<Recipe> recipeList =
        (BlocProvider.of<RecipesBloc>(context).state as RecipesLoaded).recipeList ?? [];

    for(int i = 0; i < recipeList.length;i++){
      for(int y = 0; y < recipeList[i].ingredientsFlat.length;y++){
        _selectedIngredients[recipeList[i].ingredientsFlat[y].name] = false;
      }
    }
  }

  List<Widget> _buildIngredientChoiceChips(){
    List<Widget> widgetList = [];
    _selectedIngredients.forEach((key, value){
      widgetList.add(ChoiceChip(label: Text(key), selected: value,onSelected: (isActive){
        setState(() {
          _selectedIngredients[key] = isActive;
        });
      },));
    });
    return widgetList;
  }

  void _updateFilter(){
    List<String> ingredients = [];
    _selectedIngredients.forEach((k,v){
      if(v) ingredients.add(k);
    });

    widget.filteredRecipesBloc.add(
        UpdateRecipesFilter(ingredients: ingredients.isNotEmpty ? ingredients : null));
    //BlocProvider.of<FilteredRecipesBloc>(context).add(
      //  UpdateRecipesFilter(ingredients: ingredients.isNotEmpty ? ingredients : null),);
  }
}

Solution

You don’t want StatefulWidget to controll your Bloc. You can instantiate your Bloc in initState method but you don’t need to close it via dispose method because it automatically does it for you.

If you have made an instance of the Bloc in initState, you don’t want to make another one via BlocProvider. But instead you should use the named constructor .value.

Either

  FilteredRecipesBloc _filteredRecipesBloc;

  @override
  void initState() {
    _filteredRecipesBloc = FilteredRecipesBloc(
        recipeList:
            (BlocProvider.of<RecipesBloc>(context).state as RecipesLoaded)
                .recipeList);
    super.initState();
  }

  BlocProvider.value(
    value: _filteredRecipesBloc,
    child: ...
  )

OR

  // Preferable at least for me, because I don't need to bother with the instance of the Bloc.
  BlocProvider(
    create: (context) => FilteredRecipesBloc(
        recipeList:
            (BlocProvider.of<RecipesBloc>(context).state as RecipesLoaded)
                .recipeList),
    child: ...
  )
class ExploreScreen extends StatefulWidget {
  @override
  _ExploreScreenState createState() => _ExploreScreenState();
}

class _ExploreScreenState extends State<ExploreScreen> {
  FilteredRecipesBloc _filteredRecipesBloc;

  @override
  void initState() {
    _filteredRecipesBloc = FilteredRecipesBloc(
        recipeList:
            (BlocProvider.of<RecipesBloc>(context).state as RecipesLoaded)
                .recipeList);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Explore"),
      ),
      body: BlocProvider.value(
        value: _filteredRecipesBloc,
        child: BlocBuilder<FilteredRecipesBloc, FilteredRecipesState>(
            builder: (context, state) {
          if (state is FilteredRecipeEmpty) {
            return CategoriesScreen();
          }
          if (state is FilteredRecipesLoading) {
            return Column(
              children: <Widget>[
                CircularProgressIndicator(),
                IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    _filteredRecipesBloc.add(UpdateRecipesFilter(
                        ingredients: ["Curry"], maxCookTime: 30));
                  },
                ),
              ],
            );
          }
          if (state is FilteredRecipeLoaded) {
            return ListView.builder(
                itemCount: state.recipeList.length,
                itemBuilder: (_, int index) {
                  return ImageRecipeContainer(recipe: state.recipeList[index]);
                });
          }
          return Container();
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _navigateToFilterScreen,
        child: Icon(EvaIcons.funnelOutline),
        heroTag: "fafdsf",
      ),
    );
  }

  void _navigateToFilterScreen() {
    Navigator.of(context)
        .push(MaterialPageRoute<FilterScreen>(builder: (context) {
      return BlocProvider.value(
        value: _filteredRecipesBloc,
        child: FilterScreen(_filteredRecipesBloc),
      );
    }));
  }
}

Answered By – Federick Jonathan

Answer Checked By – Willingham (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *