简体   繁体   中英

flutter_bloc : Make initialState method async

I am using the flutter_bloc package to manage state in my app. I have a usecase where I have to load the initial state from the remote DB. This requires the initialState method to be async, which it is not.

If not by using the initialState method, what is the best way to load the initial state of a Bloc from a remote DB ?

Comprehensive explanation:

The initialState of the Bloc in the flutter_bloc plugin must be sync .
because there must be an initial state immediately available when the bloc is instantiated.

So, if you want to have a state from an async source , you can call your async function inside of the mapEventToState function and emit a new state when your work is completed.

General Stpes:
step(1):
Create your own Bloc class with your desired events and states.

class YourBloc extends Bloc<YourEvent, YourState> {
  @override
  YourState get initialState => LoadingState();

  @override
  Stream<YourState> mapEventToState(YourEvent event) async* {
    if (event is InitEvent) {
      final data = await _getDataFrom_SharedPreferences_OR_Database_OR_anyAsyncSource();
      yield LoadedState(data);
    }
  }
}

where LoadingState and LoadedState can be sub classes of YourState class or same type and can have different properties to use in widgets later. Similarly, InitEvent and other your events ate also sub classes of YourEvent class or just an enum.

step(2):
Now when you wan to create BlocProvider widget, you can immediately add the initEvent like as the below:

BlocProvider<YourBloc>(
  create: (_) => YourBloc()..add(InitEvent()),
  child: YourChild(),
)

step(3):
Use different states to show different widgets:

BlocBuilder<YourBloc, YourState>(
  builder: (context, state) {
    if (state is LoadingState) {
      return Center(child: CircularProgressIndicator(),);
    }
    if (state is LoadedState) {
      return YourWidget(state.data);
    }
  }
)

Practical Example:
Please suppose we have a counter(+/-) for each product in a shopping app and we want to save the selected count of item in the SharedPreferences or database (you can use any async data source). so that whenever user opens the app, he/she could see the selected item counts.

//our events:
enum CounterEvent {increment, decrement, init}

class YourBloc extends Bloc<CounterEvent, int>{
    final Product product;
    YourBloc(int initialState, this.product) : super(initialState);

    @override
    Stream<int> mapEventToState(CounterEvent event) async* {
        int newState;
        if(event == CounterEvent.init){
            //get data from your async data source (database or shared preferences or etc.)
            newState = data.count;
            yield newState;
        }
        else if(event == CounterEvent.increment){
            newState = state + 1;
            saveNewState(newState);
            yield newState;
        }else if(event  == CounterEvent.decrement && state > 0){
            newState = state - 1;
            saveNewState(newState);
            yield newState;
        }
    }

    void saveNewState(int count){
        //save your new state in database or shared preferences or etc.
    }
}

class ProductCounter extends StatelessWidget {
  
  final Product product;
  ProductCounter(this.product);
  
  @override
  Widget build(BuildContext context) {
    return BlocProvider<YourBloc>(
        //-1 is a fake initial (sync) value that is converted to progressbar in BlocBuilder
        create: (context) => YourBloc(-1, product)..add(CounterEvent.init),
        child: YourWidget()
    );
  }
}

class YourWidget extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    
    final _yourBloc  = BlocProvider.of<YourBloc>(context);

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        FloatingActionButton(
            child: const Icon(Icons.add),
            onPressed: () => _yourBloc.add(CounterEvent.increment),
          ),
        BlocBuilder<ProductCounterBloc, int>(
              builder: (BuildContext context, int state) {
                if(state == -1){
                  return Center(child: CircularProgressIndicator(),);
                }else {
                  return Container(
                    width: 24,
                    child: Text(
                      state > 0 ? state.toString().padLeft(2, "0") : "-",
                      textAlign: TextAlign.center,
                    ),
                  );
                }
              }
            ),
        FloatingActionButton(
          child: const Icon(Icons.remove),
          onPressed: () => _yourBloc.add(CounterEvent.decrement),
        ),
      ],
    );
  }


}

You can send an event to the bloc to start loading(on it event bloc send new LoadingState ) where you receive and show Loader , then when loading ended bloc send another `state with data and you just switch loading state to loaded(and show data). You don't need to await call , what you have to do is just pushing and receiving states

Another option could be that for example in the configuration file where you have dependency injection you could await for a state over there. And that state then pass in the constructor of the bloc. So now in the bloc you can easily point the initialState to the one you passed in.

Yes you have to remember you should change the initial state when your data is ready.

Now I provide a use case for this scenario. You might have already basic option or settings displayed for the user. That simple data you get from the initial state. Then the next state: Loading state for example can display loading indicator which background has some kind of opacity. User can already see basic options while more needed data is being loaded.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM