Are states not passed to BlocBuilder widgets that are children of a BlocConsumer widget?

Issue

I am new to Flutter Bloc and must be missing how State changes are processed by the UI widgets. At the top level I have a BlocConsumer and under that I have nested BlocBuilder widgets with buildWhen methods to indicate when and how the Bloc widget should be rebuilt. Based on print statements,it looks like the Bloc state is consumed in the top level BlocConsumer widget and never makes it down to the lower level BlocBuilder widgets.

The code below should

  1. Display circular progress bar on startup – this works ok
  2. Call a bunch of APIs – This is happening
  3. In the meantime display the initial screen with default text values in various widgets – this happens
  4. As API returns and Bloc passes states on the stream, the appropriate UI widget should be rebuilt replacing default text with the data in the stream object. — this doesn’t happen.

Code snippets:

RaspDataStates issued by Bloc (Just showing for reference. Not showing all subclasses of RaspDataState):

@immutable
abstract class RaspDataState {}

class RaspInitialState extends RaspDataState {
  @override
  String toString() => "RaspInitialState";
}

class RaspForecastModels extends RaspDataState {
  final List<String> modelNames;
  final String selectedModelName;
  RaspForecastModels(this.modelNames, this.selectedModelName);
}
...

Bloc just to show how initialized. Code all seems to work fine and isn’t shown.

class RaspDataBloc extends Bloc<RaspDataEvent, RaspDataState> { 
  RaspDataBloc({required this.repository}) : super(RaspInitialState());

  @override
  RaspDataState get initialState => RaspInitialState();
  ...

Now to the UI widget.

class SoaringForecast extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<RaspDataBloc>(
      create: (BuildContext context) =>
          RaspDataBloc(repository: RepositoryProvider.of<Repository>(context)),
      child: RaspScreen(repositoryContext: context),
    );
  }
} 


class RaspScreen extends StatefulWidget {
  final BuildContext repositoryContext;
  RaspScreen({Key? key, required this.repositoryContext}) : super(key: key);
  @override
  _RaspScreenState createState() => _RaspScreenState();
}

class _RaspScreenState extends State<RaspScreen>
    with SingleTickerProviderStateMixin, AfterLayoutMixin<RaspScreen> {
 // Executed only when class created
 @override
  void initState() {
    super.initState();
    _firstLayoutComplete = false;
    print('Calling series of APIs');
    BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
    _mapController = MapController();
  }


 @override
  void afterFirstLayout(BuildContext context) {
    _firstLayoutComplete = true;
    print(
        "First layout complete. mapcontroller is set ${_mapController != null}");
    _setMapLatLngBounds();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        key: _scaffoldKey,
        drawer: AppDrawer.getDrawer(context),
        appBar: AppBar(
          title: Text('RASP'),
          actions: <Widget>[
            IconButton(icon: Icon(Icons.list), onPressed: null),
          ],
        ),
        body: BlocConsumer<RaspDataBloc, RaspDataState>(
            listener: (context, state) {
          print('In forecastLayout State: $state');  << Can see all streamed states here
          if (state is RaspDataLoadErrorState) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                backgroundColor: Colors.green,
                content: Text(state.error),
              ),
            );
          }
        }, builder: (context, state) {
          print('state is $state');   << Only see last streamed state here
          if (state is RaspInitialState || state is RaspDataLoadErrorState) {
            print('returning CircularProgressIndicator');
            return Center(
              child: CircularProgressIndicator(),
            );
          }
          print('creating main screen');   << Only see this when all streams complete
          return Padding(
              padding: EdgeInsets.all(8.0),
              child:
                  Column(mainAxisAlignment: MainAxisAlignment.start, children: [
                getForecastModelsAndDates(),
                getForecastTypes(),
                displayForecastTimes(),
                returnMap()
              ]));
        }));
  }

  Widget getForecastModelsAndDates() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Expanded(
          flex: 3,
          child: forecastModelDropDownList(), // ForecastModelsWidget()
        ),
        Expanded(
            flex: 7,
            child: Padding(
              padding: EdgeInsets.only(left: 16.0),
              child: forecastDatesDropDownList(),
            )),
      ],
    );
  }

// Display GFS, NAM, ....
  Widget forecastModelDropDownList() {
    return BlocBuilder<RaspDataBloc, RaspDataState>(
        buildWhen: (previous, current) {
      return current is RaspInitialState || current is RaspForecastModels;
    }, builder: (context, state) {
      if (state is RaspInitialState || !(state is RaspForecastModels)) {
        return Text("Getting Forecast Models");
      }
      var raspForecastModels = state;
      print('Creating dropdown for models');
      return DropdownButton<String>(
        value: (raspForecastModels.selectedModelName),
        isExpanded: true,
        iconSize: 24,
        elevation: 16,
        onChanged: (String? newValue) {
          BlocProvider.of<RaspDataBloc>(context)
              .add(SelectedRaspModel(newValue!));
        },
        items: raspForecastModels.modelNames
            .map<DropdownMenuItem<String>>((String value) {
          return DropdownMenuItem<String>(
            value: value,
            child: Text(value.toUpperCase()),
          );
        }).toList(),
      );
    });
  }

 ... more BlocBuilder child widgets similar to the one above

The print statements in the console are:

Calling series of APIs
state is RaspInitialState
returning CircularProgressIndicator
First layout complete. mapcontroller is set true
... (First of bunch of API output displays - all successful)
state is RaspInitialState                  << Not sure why this occurs again
returning CircularProgressIndicator
... (More API output displays - all successful)
streamed RaspForecastModels
In forecastLayout State: Instance of 'RaspForecastModels' << Doesn't cause widget to be rebuild
streamed RaspForecastDates     << Other states being produced by Bloc 
In forecastLayout State: Instance of 'RaspForecastDates'
streamed RaspForecasts
In forecastLayout State: Instance of 'RaspForecasts'
In forecastLayout State: Instance of 'RaspForecastTime'
streamed RaspMapLatLngBounds
In forecastLayout State: Instance of 'RaspMapLatLngBounds'
state is Instance of 'RaspMapLatLngBounds'
creating main screen

Any words of wisdom on the errors of my way would be appreciated.

Solution

I added this earlier as a comment but then found Stackoverflow didn’t initially show my comment (I needed to click on show more). So here it is in better readable form.

Problem solved. I needed to move the line:

BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections()); 

from the initState() method to afterFirstLayout().

All blocbuilders then executed and the UI was built appropriately . And to answer my title question, the bloc states are broadcast and can be picked up by different BlocBuilders.

Answered By – Eric

Answer Checked By – Marie Seifert (FlutterFixes Admin)

Leave a Reply

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