Chapters

Hide chapters

Real-World Flutter by Tutorials

First Edition · Flutter 3.3 · Dart 2.18 · Android Studio or VS Code

Real-World Flutter by Tutorials

Section 1: 16 chapters
Show chapters Hide chapters

5. Managing Complex State With Blocs
Written by Edson Bueno

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Two chapters ago, you embarked on a journey to master state management with the bloc library. You started by using a Cubit — a simplified Bloc — to manage the quote details screen. Then, in the previous chapter, you consolidated that knowledge and demonstrated how far one could go with Cubits. You learned how to use Cubits to handle what’s perhaps the most common challenge in app development: form validation. Finally, this chapter is where you step up to the real thing: Blocs.

Now, if you think of Cubits as worse than Blocs, that’s actually not the case at all: Cubits can do 95% of what Blocs can do at 60% of the complexity — numbers taken from the same source that revealed 73.6% of all numbers are made up on the spot.

The point is: You don’t stop using Cubits once you know Blocs. If this was a shooter game, Cubits would be your handguns: lighter and easier to use, thus more effective for close combat. Yet, sometimes you just need a Bloc sniper rifle and don’t care about carrying the extra weight. But that’s enough metaphors for a “real-world” book…

In this chapter, you’ll learn how to:

  • Understand the difference between Cubits and Blocs, and what that looks like in the code.
  • Communicate with a Bloc.
  • Create a Bloc.
  • Generate, manipulate and consume Streams.
  • Implement a full-fledged search bar with advanced techniques such as debouncing.
  • Determine the exact situations where you should pick Blocs over Cubits.

While going through this chapter, you’ll work on the starter project from this chapter’s assets folder.

Differentiating Between Cubits and Blocs

Both Cubits and Blocs do only two things:

  • Take in events.
  • Emit states.

Processes Events Sends events Interacts with the app User Receives visual feedback Emits states Cubit/Bloc

Events come in, and states go out. Nothing new so far, right?

Now, get this tattooed on your brain: The only difference between Cubits and Blocs is how they take in those UI events. Nothing else.

As you’ve seen from the last two chapters, Cubits take in events through functions you define inside them and then call from your widgets. For example:

UpvoteIconButton(
  onTap: () {
    if (quote.isUpvoted == true) {
      cubit.unvoteQuote();
    } else {
      cubit.upvoteQuote();
    }
  },
  // Omitted code.
)

Blocs, on the other hand, come pre-baked with an add() function you have to use for all events. For example:

UpvoteIconButton(
  onTap: () {
    if (quote.isUpvoted == true) {
      bloc.add(QuoteDetailsUnvoted());
    } else {
      bloc.add(QuoteDetailsUpvoted());
    }
  },
  // Omitted code.
)

Since you have to use one function for all your events, you differentiate between the events through the object you pass in as the add() function’s argument. You specify the type of these objects when you define the Bloc, as in:

QuoteDetailsBloc Bloc QuoteDetailsEvent, QuoteDetailsState < > { } //Ommitted code. extends class { } < > //Ommitted code. extends class QuoteDetailsCubit Cubit QuoteDetailsState vs.

This means that when using Blocs, besides having to create a state class — as you do for Cubits — you now have one extra level of complexity: creating an event class.

Throughout this chapter, though, you’ll see that this extra cost of using Blocs doesn’t come without benefits. Having all your events coming in through a single function gives you a whole lot more control over how to process these events. As a general rule, screens with search bars can benefit a lot from that.

For WonderWords specifically, the home screen is the ideal candidate for Blocs — lots of different events and a search bar — so that’s where your focus will be for the rest of this chapter.

Having gone through the last two chapters, creating state classes should no longer be a mystery to you. So, you’ll skip that part and dive right into the unknown territory: the event classes.

Creating Event Classes

Open the starter project and fetch the dependencies by running the make get command from the root directory. Ignore any errors in the code for now and open quote_list_event.dart inside the quote_list feature package.

// 1
abstract class QuoteListEvent extends Equatable {
  const QuoteListEvent();

  @override
  List<Object?> get props => [];
}

// 2
class QuoteListFilterByFavoritesToggled extends QuoteListEvent {
  const QuoteListFilterByFavoritesToggled();
}

// 3
class QuoteListTagChanged extends QuoteListEvent {
  const QuoteListTagChanged(
    this.tag,
  );

  final Tag? tag;

  @override
  List<Object?> get props => [
        tag,
      ];
}

// 4
class QuoteListSearchTermChanged extends QuoteListEvent {
  const QuoteListSearchTermChanged(
    this.searchTerm,
  );

  final String searchTerm;

  @override
  List<Object?> get props => [
        searchTerm,
      ];
}

// 5
class QuoteListRefreshed extends QuoteListEvent {
  const QuoteListRefreshed();
}
{ } ZuajaLeybJdab Skof RoaxoDidsEdiny, BaoxeFunjYcila < > //Edmivsik jiho. ucsutxz mrijp

class QuoteListNextPageRequested extends QuoteListEvent {
  const QuoteListNextPageRequested({
    required this.pageNumber,
  });

  final int pageNumber;
}

abstract class QuoteListItemFavoriteToggled extends QuoteListEvent {
  const QuoteListItemFavoriteToggled(
    this.id,
  );

  final int id;
}

class QuoteListItemFavorited extends QuoteListItemFavoriteToggled {
  const QuoteListItemFavorited(
    int id,
  ) : super(id);
}

class QuoteListItemUnfavorited extends QuoteListItemFavoriteToggled {
  const QuoteListItemUnfavorited(
    int id,
  ) : super(id);
}
YiuweKofzAcesl MiogiRokjEmexTotohuqey FaanoWuzjUnidEnberixeget PoujeGoktGiqpebXcBoxificedGuqmyof XoasiNohlLoxmHiboFaleujkak CoayuGiqmEkizCudovanaTugjsos - VauwiDohkBeehsqFehbFjodhem MiosiDuhdSapgonzih SaisaQernKubTfickoj

class QuoteListFailedFetchRetried extends QuoteListEvent {
  const QuoteListFailedFetchRetried();
}

class QuoteListUsernameObtained extends QuoteListEvent {
  const QuoteListUsernameObtained();
}

class QuoteListItemUpdated extends QuoteListEvent {
  const QuoteListItemUpdated(
    this.updatedQuote,
  );

  final Quote updatedQuote;
}

Forwarding the Events to the Bloc

Open quote_list_screen.dart, which lives under the same directory you’ve been working on.

_pagingController.addPageRequestListener((pageNumber) {
  final isSubsequentPage = pageNumber > 1;
  if (isSubsequentPage) {
    _bloc.add(
      QuoteListNextPageRequested(
        pageNumber: pageNumber,
      ),
    );
  }
});
_searchBarController.addListener(() {
  _bloc.add(
    QuoteListSearchTermChanged(
      _searchBarController.text,
    ),
  );
});
_bloc.add(
  const QuoteListRefreshed(),
);

bloc.add(
  isFavorite
    ? QuoteListItemUnfavorited(quote.id)
    : QuoteListItemFavorited(quote.id),
);
// 1
final updatedQuote = await onQuoteSelected(quote.id);
                      
if (updatedQuote != null &&
    // 2
    updatedQuote.isFavorite != quote.isFavorite) {
  // 3
  bloc.add(
    QuoteListItemUpdated(
      updatedQuote,
    ),
  );
}
bloc.add(
  const QuoteListFailedFetchRetried(),
);

Scaffolding the Bloc

Still in the same directory you’ve been working on, open quote_list_bloc.dart.

// 1
class QuoteListBloc extends Bloc<QuoteListEvent, QuoteListState> {
  QuoteListBloc({
    required QuoteRepository quoteRepository,
    required UserRepository userRepository,
  })  :
        // 2 
        _quoteRepository = quoteRepository,
        // 3
        super(
          const QuoteListState(),
        ) {
    // 4
    _registerEventHandler();
    
    // TODO: Watch the user's authentication status.
  }

  // 5
  late final StreamSubscription _authChangesSubscription;
  String? _authenticatedUsername;
  
  final QuoteRepository _quoteRepository;

  // Omitted code.
}
_authChangesSubscription = userRepository
    // 1
    .getUser()
    // 2
    .listen(
  (user) {
    // 3
    _authenticatedUsername = user?.username;

    // 4
    add(
      const QuoteListUsernameObtained(),
    );
  },
);
@override
Future<void> close() {
  _authChangesSubscription.cancel();
  return super.close();
}

Fetching Data

Replace // TODO: Create a utility function that fetches a given page. with:

Stream<QuoteListState> _fetchQuotePage(
  int page, {
  required QuoteListPageFetchPolicy fetchPolicy,
  bool isRefresh = false,
}) async* {
  // 1
  final currentlyAppliedFilter = state.filter;
  // 2
  final isFilteringByFavorites = currentlyAppliedFilter is QuoteListFilterByFavorites;
  // 3
  final isUserSignedIn = _authenticatedUsername != null;
  if (isFilteringByFavorites && !isUserSignedIn) {
    // 4
    yield QuoteListState.noItemsFound(
      filter: currentlyAppliedFilter,
    );
  } else {
    // TODO: Fetch the page.
  }
}
final pageStream = _quoteRepository.getQuoteListPage(
  page,
  tag: currentlyAppliedFilter is QuoteListFilterByTag
      ? currentlyAppliedFilter.tag
      : null,
  searchTerm: currentlyAppliedFilter is QuoteListFilterBySearchTerm
      ? currentlyAppliedFilter.searchTerm
      : '',
  favoritedByUsername:
      currentlyAppliedFilter is QuoteListFilterByFavorites
          ? _authenticatedUsername
          : null,
  fetchPolicy: fetchPolicy,
);

try {
  // 1
  await for (final newPage in pageStream) {
    final newItemList = newPage.quoteList;
    final oldItemList = state.itemList ?? [];
    // 2
    final completeItemList = isRefresh || page == 1
        ? newItemList
        : (oldItemList + newItemList);
    
    final nextPage = newPage.isLastPage ? null : page + 1;

    // 3
    yield QuoteListState.success(
      nextPage: nextPage,
      itemList: completeItemList,
      filter: currentlyAppliedFilter,
      isRefresh: isRefresh,
    );
  }
} catch (error) {
  // TODO: Handle errors.
}
if (error is EmptySearchResultException) {
  // 1
  yield QuoteListState.noItemsFound(
    filter: currentlyAppliedFilter,
  );
}

if (isRefresh) {
  // 2
  yield state.copyWithNewRefreshError(
    error,
  );
} else {
  // 3
  yield state.copyWithNewError(
    error,
  );
}

Receiving Events

Inside the _registerEventHandler() function, replace // TODO: Take in the events. with:

// 1
on<QuoteListEvent>(
  // 2
  (event, emitter) async {
    // 3
    if (event is QuoteListUsernameObtained) {
      await _handleQuoteListUsernameObtained(emitter);
    } else if (event is QuoteListFailedFetchRetried) {
      await _handleQuoteListFailedFetchRetried(emitter);
    } else if (event is QuoteListItemUpdated) {
      _handleQuoteListItemUpdated(emitter, event);
    } else if (event is QuoteListTagChanged) {
      await _handleQuoteListTagChanged(emitter, event);
    } else if (event is QuoteListSearchTermChanged) {
      await _handleQuoteListSearchTermChanged(emitter, event);
    } else if (event is QuoteListRefreshed) {
      await _handleQuoteListRefreshed(emitter, event);
    } else if (event is QuoteListNextPageRequested) {
      await _handleQuoteListNextPageRequested(emitter, event);
    } else if (event is QuoteListItemFavoriteToggled) {
      await _handleQuoteListItemFavoriteToggled(emitter, event);
    } else if (event is QuoteListFilterByFavoritesToggled) {
      await _handleQuoteListFilterByFavoritesToggled(emitter);
    }
  },
  // TODO: Customize how events are processed.
);
QiexeDujhEcamg CeiduCuptOcexTebekekah SiuliWursEgohApkanofeqez CiosuRipqDudwopMwKilozofumXonnwen NaubeMafsCiytBoyoZesuenvab XuofoTunbItenGotebetoTewmveh RaosoRefyNeudumFavjdadHixceix QoeniCigwOnumjeloEgwiemel ZautoGigzOjucUbmevew BaihaCakdTiugkwYatvLnajjax BiohoJibvZesmodhow GaaraWotqFujHzeqtuk

Handling Individual Events

Scroll down and find // TODO: Handle QuoteListUsernameObtained.. Replace it with:

// 1
emitter(
 QuoteListState(
    filter: state.filter,
  ),
);

// 2
final firstPageFetchStream = _fetchQuotePage(
  1,
  fetchPolicy: QuoteListPageFetchPolicy.cacheAndNetwork,
);

// 3
return emitter.onEach<QuoteListState>(
  firstPageFetchStream,
  onData: emitter,
);

Controlling the Traffic of Events

Now, changing subjects, your home screen is in pretty good shape already, but have you tried using the search bar? You’d find two problems with it:

Knowing the Transformer Function

Get back to the code, and, continuing on your Bloc’s file, replace // TODO: Customize how events are processed. with:

transformer: (eventStream, eventHandler) {
  // TODO: Debounce search events.

  // TODO: Discard in-progress event if a new one comes in.
},

Applying the Debouncing Effect

Pick up where you left off by replacing // TODO: Debounce search events. with:

// 1
final nonDebounceEventStream = eventStream.where(
  (event) => event is! QuoteListSearchTermChanged,
);

final debounceEventStream = eventStream
    // 2
    .whereType<QuoteListSearchTermChanged>()
    // 3
    .debounceTime(
      const Duration(seconds: 1),
    )
    // 4
    .where((event) {
  final previousFilter = state.filter;
  final previousSearchTerm =
      previousFilter is QuoteListFilterBySearchTerm
          ? previousFilter.searchTerm
          : '';

  final isSearchNotAlreadyDisplayed = event.searchTerm != previousSearchTerm;
  return isSearchNotAlreadyDisplayed;
});

// 5
final mergedEventStream = MergeStream([
  nonDebounceEventStream,
  debounceEventStream,
]);

Applying the Canceling Effect

By default, Blocs process all incoming events in parallel. It works extremely well for the majority of situations — so much so that this is how all Cubits work — but it ends up being a problem for search bars:

Liocbb “mi” “geb” “ginu” Myodomxahr Puqa

Qeatws “so” “lat” “vodi” Qbijocmiwv Jema

// 1
final restartableTransformer = restartable<QuoteListEvent>();

// 2
return restartableTransformer(mergedEventStream, eventHandler);
cagdahxigh hwahogk equhth yiygipjalzjk (bxiv’y kefievs) locoazsuuz nfiforf uxuhcr waluufjuibsq rlaqbolti iqjufa emb oxoyvb ukzey bdake uy esugv oh dgiwavwitf nurvugjajya zvodolf ebgw yge zuyogs ehutj osd bikfat klavoeuq ulury racnkurh

Key Points

  • You don’t stop using Cubits once you know Blocs; one isn’t better than the other.
  • The only difference between Cubits and Blocs is how they receive events from the UI layer.
  • While Cubits require you to create one function for each event, Blocs require you to create one class for each.
  • As a rule of thumb, default to Cubits for their simplicity. Then, if you find you need to control the traffic of events coming in, upgrade to Blocs.
  • If you don’t customize the transformer, your Bloc will perform just like a Cubit: processing events one by one as they arrive and not considering their order when handing over the results.
  • The ability to customize how events are processed is why you should pick a Bloc over a Cubit.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now