Triggering initial event in BLoC
Asked Answered
M

3

36

example_states:

abstract class ExampleState extends Equatable {
  const ExampleState();
}

class LoadingState extends ExampleState {
  //
}

class LoadedState extends ExampleState {
  //
}

class FailedState extends ExampleState {
  //
}

example_events:

abstract class ExampleEvent extends Equatable {
  //
}

class SubscribeEvent extends ExampleEvent {
  //
}

class UnsubscribeEvent extends ExampleEvent {
  //
}

class FetchEvent extends ExampleEvent {
  // 
}

example_bloc:

class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
  @override
  ExampleState get initialState => LoadingState();

  @override
  Stream<ExampleState> mapEventToState(
    ExampleEvent event,
  ) async* {
    if (event is SubscribeEvent) {
      //
    } else if (event is UnsubscribeEvent) {
      //
    } else if (event is FetchEvent) {
      yield LoadingState();
      try {
        // network calls
        yield LoadedState();
      } catch (_) {
        yield FailedState();
      }
    }
  }
}

example_screen:

class ExampleScreenState extends StatelessWidget {
  // ignore: close_sinks
  final blocA = ExampleBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocBuilder<ExampleBloc, ExampleState>(
        bloc: blocA,
        // ignore: missing_return
        builder: (BuildContext context, state) {
          if (state is LoadingState) {
            blocA.add(Fetch());
            return CircularProgressBar();
          }

          if (state is LoadedState) {
            //...
          }

          if (state is FailedState) {
            //...
          }
        },
      ),
    );
  }
}

As you can see in example_bloc, initial state is LoadingState() and in build it shows circular progress bar. I use Fetch() event to trigger next states. But I don't feel comfortable using it there. What I want to do is:

When app starts, it should show LoadingState and start networking calls, then when it's all completed, it should show LoadedState with networking call results and FailedState if something goes wrong. I want to achieve these without doing

if (state is LoadingState) {
  blocA.add(Fetch());
  return CircularProgressBar();
}
Metic answered 29/6, 2020 at 23:24 Comment(0)
I
76

Your discomfort really has reason - no event should be fired from build() method (build() could be fired as many times as Flutter framework needs)

Our case is to fire initial event on Bloc creation

Possibilities overview

  1. case with inserting Bloc with BlocProvider - this is preferred way

create: callback is fired only once when BlocProvider is mounted & BlocProvider would close() bloc when BlocProvider is unmounted

    class ExampleScreenState extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: BlocProvider(
            create: (context) => ExampleBloc()..add(Fetch()), // <-- first event, 
            child: BlocBuilder<ExampleBloc, ExampleState>(
              builder: (BuildContext context, state) {
                ...
              },
            ),
          ),
        );
      }
    }
  1. case when you create Bloc in State of Statefull widget
class _ExampleScreenStateState extends State<ExampleScreenState> {
  ExampleBloc _exampleBloc;

  @override
  void initState() {
    super.initState();
    _exampleBloc = ExampleBloc();
    _exampleBloc.add(Fetch());
    // or use cascade notation
    // _exampleBloc = ExampleBloc()..add(Fetch());
  }

  @override
  void dispose() {
    super.dispose();
    _exampleBloc.close(); // do not forget to close, prefer use BlocProvider - it would handle it for you
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocBuilder<ExampleBloc, ExampleState>(
        bloc: _exampleBloc,
        builder: (BuildContext context, state) {
         ...
        },
      ),
    );
  }
}
  1. add first event on Bloc instance creation - this way has drawbacks when testing because first event is implicit
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {

  ...

  ExampleBloc() {
    add(Fetch());
  }
}

// insert it to widget tree with BlocProvider or create in State
BlocProvider( create: (_) => ExampleBloc(), ...

// or in State

class _ExampleScreenStateState extends State<ExampleScreenState> {
  final _exampleBloc = ExampleBloc(); 
...

PS feel free to reach me in comments

Iridaceous answered 30/6, 2020 at 7:15 Comment(7)
Thank you very much, these really helped a lotMetic
When I do ExampleBloc()..add(Fetch() in BlocProvider, it will makes the unit-test of the page failed with A Timer is still pending even after the widget tree was disposed., how to fix it?Duna
Sorry, I have no clue - because we talk about first Event for Bloc. Create new question with code snippet of test & code of Widget you testingIridaceous
Thanks for explaining clearly. Got a clearer picture from your answer.Cantankerous
Looks like the third case is what I was looking for. Thanks you!Shalloon
when I've tried the second case BlocListener does not work.Cooperman
create: (context) => ExampleBloc()..add(Fetch()), will throw an error in latest version of bloc package. 'package:flutter/src/widgets/framework.dart': Failed assertion: line 6151 pos 12: 'renderObject.child == child': is not true.Chigger
K
10

Sergey Salnikov has a great answer. I think I can add another suggestion however.

In my main.dart file I am using a MultiBlocProvider to create all my blocs for use further down the tree. Like so

Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: <BlocProvider<dynamic>>[
        BlocProvider<OneBloc>(create: (_) => OneBloc()),
        BlocProvider<TwoBloc>(create: (_) => TwoBloc()),
      ],
      child: MaterialApp( // Rest of your app )

Then when I need to call an event when I load a page, in this case I wanted to fetch some data depending on a list tile selected, and I needed more options than FutureBuilder can provide me, I simple used initState(); and called the bloc provider and added an event.

class _ExampleScreenState extends State<ExampleScreen> {
    

  @override
  void initState() {
    super.initState();
    BlocProvider.of<OneBloc>(context)
        .add(FetchData);
  }

It works because the bloc has already been provided from the root widget.

Klepac answered 16/8, 2021 at 14:37 Comment(2)
I've tried this but It still says "This can happen if the context you used comes from a widget above the BlocProvider".Cooperman
@Cooperman make sure the BlocProvider is at the very top of the app.Klepac
A
7

In simple terms:

1. Using BlocProvider, call it during creation.

   BlocProvider(create: (context) => ExampleBloc()..add(Fetch()))

2. Using BlocState, use it as

   class _ExampleScreenStateState extends State<ExampleScreenState> {
      ExampleBloc _exampleBloc;

      @override
     void initState() {
     super.initState();
     _exampleBloc = ExampleBloc()..add(Fetch());
   }

   @override
   void dispose() {
     super.dispose();
     _exampleBloc.close();
  }
Adlar answered 15/12, 2022 at 3:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.