Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ?

like image 902
sharath Avatar asked Feb 24 '20 05:02

sharath


2 Answers

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


}

like image 93
Ghasem Sadeghi Avatar answered Oct 31 '22 23:10

Ghasem Sadeghi


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

like image 33
Yauhen Sampir Avatar answered Oct 31 '22 23:10

Yauhen Sampir