Getting Started With the BLoC Pattern

See how to use the popular BLoC pattern to build your Flutter app architecture and manage the flow of data through your widgets using Dart streams. By Sardor Islomov.

5 (5) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

The Touch of RxDart

At this moment, you can search articles and see results. But there are a few UX and performance issues you can solve:

  • ArticleListBloc sends a network request every time you change the search field character by character. Usually, users want to enter a reasonable query and see results for it. To solve this problem, you’ll debounce the input events and send a request when the user completes their query. Debouncing means the app skips input events that come in short intervals.
  • When you finish entering your query, you might think the screen is stuck because you don’t see any UI feedback. To improve the user experience, show the user the app is loading and isn’t stuck.
  • asyncMap waits for request completion, so the user sees all entered query responses one by one. Usually, you have to ignore the previous request result to process a new query.

The main purpose of BLoC is to model Business Logic components. Thanks to this, you can solve the previous issues by editing BLoC code only without editing widgets at all on the UI layer.

Go to bloc/article_list_bloc.dart and add import 'package:rxdart/rxdart.dart'; at the top of the file. rxdart packages are already added in pubspec.yaml.

Replace ArticleListBloc() with the following:

ArticleListBloc() {
  articlesStream = _searchQueryController.stream
      .startWith(null) // 1
      .debounceTime(const Duration(milliseconds: 100)) // 2
      .switchMap( // 3
        (query) => _client.fetchArticles(query)
            .asStream() // 4
            .startWith(null), // 5
      );
}

The code above changes the output stream of articles in the following way:

  1. startWith(null) produces an empty query to start loading all articles. If the user opens the search for the first time and doesn’t enter any query, they see a list of recent articles.
  2. debounceTime skips queries that come in intervals of less than 100 milliseconds. When the user enters characters, TextField sends multiple onChanged{} events. debounce skips most of them and returns the last keyword event.
    Note: Read more about the debounce operator at ReactiveX – debounce documentation
  3. Replace asyncMap with switchMap. These operators are similar, but switchMap allows you to work with other streams.
  4. Convert Future to Stream.
  5. startWith(null) at this line sends a null event to the article output at the start of every fetch request. So when the user completes the search query, UI erases the previous list of articles and shows the widget’s loading. It happens because _buildResults in article_list_screen.dart listens to your stream and displays a loading indicator in the case of null data.
Note: Read more about the debounce operator at ReactiveX – debounce documentation

Build and run the app. The app is more responsive. You see a loading indicator and only the latest entered requests.

Article list screen - completed

Final Screen and BLoC

The second screen of the app shows a detail of the article. It also has its own BLoC objects to manage the state.

Create a file called article_detail_bloc.dart in the bloc folder with the following code:

class ArticleDetailBloc implements Bloc {
  final String id;
  final _refreshController = StreamController<void>();
  final _client = RWClient();

  late Stream<Article?> articleStream;

  ArticleDetailBloc({
    required this.id,
  }) {
    articleStream = _refreshController.stream
        .startWith({})
        .mapTo(id)
        .switchMap(
          (id) => _client.getDetailArticle(id).asStream(),
    )
    .asBroadcastStream();
  }

  @override
  void dispose() {
    _refreshController.close();
  }
}

This code is very similar to ArticleListBloc. The difference is the API and the data type that’s returned. You’ll add refresh later to see another way to send input events. You need asBroadcastStream() here to allow multiple stream subscriptions for the refresh functionality.

Now, create an article_detail_screen.dart file with an ArticleDetailScreen class in the UI folder to put the new BLoC to use.

class ArticleDetailScreen extends StatelessWidget {
  const ArticleDetailScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 1
    final bloc = BlocProvider.of<ArticleDetailBloc>(context);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Articles detail'),
      ),
      body: Container(
        alignment: Alignment.center,
        // 2
        child: _buildContent(bloc),
      ),
    );
  }

  Widget _buildContent(ArticleDetailBloc bloc) {
    return StreamBuilder<Article?>(
      stream: bloc.articleStream,
      builder: (context, snapshot) {
        final article = snapshot.data;
        if (article == null) {
          return const Center(child: CircularProgressIndicator());
        }
        // 3
        return ArticleDetail(article);
      },
    );
  }
}

ArticleDetailScreen does the following:

  1. Fetches the ArticleDetailBloc instance.
  2. The body: property displays the content with data received from ArticleDetailBloc.
  3. Displays details using prepared widget ArticleDetail.

Build and run the app. After seeing an article list, tap one of them.

Article details not opening

It doesn’t navigate to ArticleDetailScreen.

That’s because you didn’t add navigation from ArticleListScreen to ArticleDetailScreen. Go to article_list_screen.dart and replace the code of the onTap{} property in _buildSearchResults() with the following:

onTap: () {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => BlocProvider(
        bloc: ArticleDetailBloc(id: article.id),
        child: const ArticleDetailScreen(),
      ),
    ),
  );
},

Build and run the app, then tap the article. It displays a detail screen of the selected article.

Articles detail screen

Next, you’ll implement the missing bit to refresh the content to fetch the latest updates or reload after a network error.

Replace body:property in article_detail_screen.dart with following code:

...
// 1
body: RefreshIndicator(
  // 2
  onRefresh: bloc.refresh,
  child: Container(
    alignment: Alignment.center,
    child: _buildContent(bloc),
  ),
),
...

Here’s a breakdown:

  1. The RefreshIndicator widget allows use of the swipe-to-refresh gesture and invokes onRefresh method.
  2. onRefresh may use BLoC sink bloc.refresh.add, but there’s a problem. onRefresh needs to get some Future back to know when to hide the loading indicator. To provide this, you’ll create a new BLoC method Future refresh() to support RefreshIndicator functionality.

Add a new method, Future refresh(), to article_detail_bloc.dart:

Future refresh() {
  final future = articleStream.first;
  _refreshController.sink.add({});
  return future;
}

The code above solves two cases: requesting an update and returning Future for RefreshIndicator. It:

  • Sends a new refresh event to sink so ArticleDetailBloc will refresh the article data.
  • The operator first of the Stream instance returns Future, which completes when any article is available in the stream at the time of this call. It helps to wait when the article update is available to render.
  • Do you remember the asBroadcastStream() call before? It’s required because of this line. first creates another subscription to articleStream.
Note: Dart stream doesn’t allow waiting for an event after you send something to sink in a simple way. This code can fail in rare cases when refresh is called at the same time an API fetch is in progress. Returned Future completes early, then the new update comes to articleStream and RefreshIndicator hides itself before the final update. It’s also wrong to send an event to sink and then request the first future. If a refresh event is processed immediately and a new Article comes before the call of first, the user sees infinity loading.

Build and run the app. It should support the swipe-to-refresh gesture.

Article detail refresh

Looks elegant! Now, users of raywenderlich.com can view and search their favorite articles from the app.