Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

preface

In our last article, we looked at BlocProvider usage, and it feels almost the same as a Provider. There was an odd BlocBuilder in the last post, so let’s review the code:

BlocBuilder<CounterCubit, int>(
  builder: (context, count) => Text(
    '$count',
    style: TextStyle(
      fontSize: 32,
      color: Colors.blue,
    ),
  ),
Copy the code

The count in this column automatically changes with the BlocProvider’s state object, but we don’t see any binding action. For example, we use the Context. watch method with the Provider, but we don’t see it here. What’s going on here? This article introduces you to BlocBuilder.

Binding of BlocBuilder to a status object

The definition of BlocBuilder in the flutter_bloc source code is as follows:

class BlocBuilder<B extends BlocBase<S>, S> extends BlocBuilderBase<B.S> {
  const BlocBuilder({
    Key? key,
    required this.builder,
    B? bloc,
    BlocBuilderCondition<S>? buildWhen,
  }) : super(key: key, bloc: bloc, buildWhen: buildWhen);

  final BlocWidgetBuilder<S> builder;

  @override
  Widget build(BuildContext context, S state) => builder(context, state);
}
Copy the code

There are two ways to bind a state object. When no bloc parameter is specified, it automatically looks up the matching state object via BlocProvider and context. This code is implemented in the State object of its parent class BlocBuilderBase (which is a StatefulWidget), and is actually done using context.read.

@override
void initState() {
  super.initState();
  _bloc = widget.bloc ?? context.read<B>();
  _state = _bloc.state;
}
Copy the code

If the Bloc parameter is specified, the specified Bloc object is used. This allows you to use your own bloc object without the BlocProvider providing it. This usage is a bit like GetBuilder for GetX. Our counter application, for example, can be simplified as follows.

class BlocBuilderDemoPage extends StatelessWidget {
  final counterCubit = CounterCubit();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc counter '),
      ),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) => Text(
            '$count',
            style: TextStyle(
              fontSize: 32,
              color: Colors.blue,
            ),
          ),
          bloc: counterCubit,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counterCubit.increment();
        },
        tooltip: 'Click Add', child: Icon(Icons.add), ), ); }}Copy the code

Refresh by condition

BlocBuilder also takes a buildWhen argument, which is a callback method that returns a bool:

typedef BlocBuilderCondition<S> = bool Function(S previous, S current);
Copy the code

That is, we can decide whether to refresh the interface based on the state before and after. For example, if the data is consistent before and after the refresh, there is no need to refresh the interface. Let’s verify it with the fake gold digger homepage we wrote before. Here we construct four states to complete the network request business:

enum LoadingStatus {
  loading, / / load
  success, // The load succeeded
  failed,  // Load failed
}
Copy the code

Then, the event mode of Bloc was used to define 3 events.

abstract class PersonalEvent {}
// Get the data event
class FetchEvent extends PersonalEvent {}
// Successful event
class FetchSucessEvent extends PersonalEvent {}
// Failed event
class FetchFailedEvent extends PersonalEvent {}
Copy the code

It also defines a state data class for the response, aggregating the personal information object with the load state.

class PersonalResponse {
  PersonalEntity? personalProfile;
  LoadingStatus status = LoadingStatus.loading;

  PersonalResponse({this.personalProfile, required this.status});
}
Copy the code

The code implementation of PersonalBloc is as follows, and the way we deal with the three events is as follows:

  • FetchEvent: Request network data;
  • FetchSucessEvent: After a successful load, build a new one with the requested personal information object and the loading statusPersonalResponseObject, usingemitRefresh the notification interface;
  • FetchFailedEvent: Failed to load, emptyPersonalResponsePersonal information object, and mark loading status as failed.
class PersonalBloc extends Bloc<PersonalEvent.PersonalResponse> {
  final String userId;
  PersonalEntity? _personalProfile;
  PersonalBloc(PersonalResponse initial, {required this.userId})
      : super(initial) {
    on<FetchEvent>((event, emit) {
      getPersonalProfile(userId);
    });
    on<FetchSucessEvent>((event, emit) {
      emit(PersonalResponse(
        personalProfile: _personalProfile,
        status: LoadingStatus.success,
      ));
    });
    on<FetchFailedEvent>((event, emit) {
      emit(PersonalResponse(
        personalProfile: null,
        status: LoadingStatus.failed,
      ));
    });
    on<RefreshEvent>((event, emit) {
      getPersonalProfile(userId);
    });
    add(FetchEvent());
  }

  void getPersonalProfile(String userId) async {
    _personalProfile = await JuejinService().getPersonalProfile(userId);
    if(_personalProfile ! =null) {
      add(FetchSucessEvent());
    } else{ add(FetchFailedEvent()); }}}Copy the code

In the constructor we request the data directly (or let the interface control it). The page implementation is similar to that of GetX, except that we use BlocBuilder to do this. The code is as follows:

class PersonalHomePage extends StatelessWidget {
  PersonalHomePage({Key? key}) : super(key: key);
  final personalBloc = PersonalBloc(
      PersonalResponse(
        personalProfile: null,
        status: LoadingStatus.loading,
      ),
      userId: '70787819648695');

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PersonalBloc, PersonalResponse>(
      bloc: personalBloc,
      builder: (_, personalResponse) {
        print('build PersonalHomePage');
        if (personalResponse.status == LoadingStatus.loading) {
          return Center(
            child: Text('Loading... ')); }if (personalResponse.status == LoadingStatus.failed) {
          return Center(
            child: Text('Request failed')); } PersonalEntity personalProfile = personalResponse.personalProfile! ;return Stack(
          children: [
            CustomScrollView(
              slivers: [
                _getBannerWithAvatar(context, personalProfile),
                _getPersonalProfile(personalProfile),
                _getPersonalStatistic(personalProfile),
              ],
            ),
            Positioned(
              top: 40,
              right: 10,
              child: IconButton(
                onPressed: () {
                  personalBloc.add(FetchEvent());
                },
                icon: Icon(
                  Icons.refresh,
                  color: Colors.white,
                ),
              ),
            ),
          ],
        );
      },
      buildWhen: (previous, next) {
        if (previous.personalProfile == null || next.personalProfile == null) {
          return true;
        }
        returnprevious.personalProfile! .userId ! = next.personalProfile! .userId; }); }// Other code omitted
}
Copy the code

Here we have added a refresh button, each click will raise a FetchEvent to request new data, and use print to print the refresh screen in BlocBuilder. But we built a buildWhen parameter to refresh the interface only if the current and subsequent user ids are inconsistent (objects can actually be compared as well, requiring overloading PersonalEntity’s == and hashCode methods) to reduce unnecessary interface refreshes. Then we comment out this line of code and return true, which means refresh each time. Let’s see how the two effects compare.

As you can see, clicking the refresh button does not refresh the screen after using the criteria, because we all use the same userId. If you comment it out and return true, it will refresh every time you click the refresh button. In this way, we can reduce unnecessary interface refreshes when the user refreshes but the data doesn’t change. The full source can be downloaded here: BLoC State Management source.

conclusion

As you can see from this article, the use of BlocBuilder is fairly simple. Bloc’s event mode is actually the same as Redux’s. Using Redux’s middleware to implement asynchronous state management) is similar in that the user triggers an event and responds to the event by returning a new data object in state management to trigger an interface refresh. BlocBuilder can either work with BlocProvider to use Bloc objects in the component tree, or it can have its own Bloc object. The buildWhen callback reduces unnecessary interface refreshes by comparing state data before and after to determine whether to refresh the interface.

I am dao Code Farmer with the same name as my wechat official account. This is a column about the introduction and practice of Flutter, providing systematic learning articles about Flutter. See the corresponding source code here: The source code of Flutter Introduction and Practical column. If you have any questions, please add me to the wechat account: island-coder.

👍🏻 : feel the harvest please point a praise to encourage!

🌟 : Collect articles, easy to look back!

💬 : Comment exchange, mutual progress!