Original text: zhuanlan.zhihu.com/p/461244648

Learn how to build a Flutter application using the popular BLoC mode and manage data flows through Widgets using Dart Streams.

Designing the structure of an application is often one of the most hotly debated topics in application development. Everyone seems to have their favorite architectural pattern with fancy acronyms.

IOS and Android developers are well-versed in model-View-Controller (MVC) as the default choice for building applications. The Model and View are separate, and the Controller is responsible for sending signals between them.

However, Flutter introduces a new responsive style that is not fully compatible with MVC. A variant of this classic pattern has appeared in the Flutter community — BLoC.

BLoC stands for Business Logic Components. BLoC’s main idea is that everything in the app should be represented as an event stream: some widgets send events; The other widgets respond. BloC is in the middle and manages these sessions. Dart even provides syntax for processing flows, which has been built into the language.

The best part about this mode is that you don’t need to import any plug-ins or learn any custom syntax. The Flutter itself already contains everything you need.

In this tutorial, you will create an app that uses the API provided by Zomato to find restaurants. At the end of the tutorial, the app will do the following:

  1. Encapsulate API calls using BLoC mode
  2. Search for restaurants and display results asynchronously
  3. Maintain a list of favorites and display them on multiple pages

Ready to start

Download and use your favorite IDE to open the Starter project. This tutorial will use Android Studio, or Visual Studio Code if you prefer. Be sure to run flutter Packages GET at the command line or IDE prompt to download the latest version of the HTTP package.

The Starter project includes some basic data models and network files. When you open your project, it should look like this:

There are three files here to communicate with Zomato.

Obtain Zomato API Key

Before you can start building your app, you need to get an API key. Jump to page developers.zomato.com/api Zomato developers, to create an account, and generate a new key.

Dart: Open zomato_client.dart in DataLayer and modify the constants in the class declaration:

class ZomatoClient {
  final _apiKey = 'PASTE YOUR API KEY HERE'; .Copy the code

Note: Best practice for production-level apps is not to store API keys in source code or VCS (version control system). It is best to read from a configuration file imported from somewhere else when building the app.

Build and run the project, which displays a blank interface.

There’s nothing to get excited about, is there? It’s time to change it.

Let’s bake a sandwich cake

It is important to organize classes in layers when writing applications, whether using Flutter or some other framework. It’s more of an informal agreement; It’s not something concrete that you can see in code.

Each layer, or set of classes, is responsible for a specific task. The starter project has a directory named DataLayer, a DataLayer that is responsible for the application’s data model and communication with the back-end server, but it knows nothing about the UI.

Each project works slightly differently, but in general, the general structure is as follows:

This architectural convention is not that different from classic MVC. The UI/Flutter layer can only communicate with the BLoC layer. The BLoC layer sends events to the data layer and the UI layer, processing business logic at the same time. As the functionality of the application grows, this structure can be easily extended.

BLoC in depth

Streams, like futures, are provided by the Dart: Async package. A stream is similar to a Future, except that a Future asynchronously returns a single value, but a stream can produce multiple values over time. If a Future is a value that will eventually be provided, then a stream is a series of values that will be provided sporadically over time.

The Dart: Async package provides an object called StreamController. StreamController is the manager object that instantiates stream and sink. Sink is the opposite of stream. Stream continuously produces output, sink continuously receives input.

In summary, BLoCs are entities that handle and store business logic, receive input data through sinks, and provide data output through streams.

Location of the page

Before using the app to find a suitable place to eat, you need to tell Zomato which geographical location you want to eat in. In this section, you create a simple page with a header search area and a list of search results.

Note: Before entering these code examples, don’t forget to open DartFmt. It is the only way to preserve the style of Flutter application code.

In the project’s lib/UI directory, create a new file named location_screen.dart. Add an extension class for the StatelessWidget to the file named LocationScreen:

import 'package:flutter/material.dart';
class LocationScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Where do you want to eat? ')),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(), hintText: 'Enter a location'),
              onChanged: (query) { },
            ),
          ),
          Expanded(
            child: _buildResults(),
          )
        ],
      ),
    );
  }


  Widget _buildResults() {
    return Center(child: Text('Enter a location')); }}Copy the code

The Location page contains a TextField where the user can enter geolocation information.

Note: The IDE prompts an error when typing classes because they are not imported. To fix this, move the cursor to any symbol with a red underscore, and then, on macOS, press Option +Enter (Alt+Enter on Windows/Linux) or click the red bulb. A menu will pop up from which you can select the correct file to import.

Create another file, main_screen.dart, to manage the page flow of your app. Add the following code to the file:

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnLocationScreen(); }}Copy the code

Finally, update main.dart to return to the new page.

MaterialApp(
  title: 'Restaurant Finder',
  theme: ThemeData(
    primarySwatch: Colors.red,
  ),
  home: MainScreen(),
),
Copy the code

Building and running your app should look like this:

It’s better than before, but it still doesn’t do anything. It’s time to create some BLoC.

The first BLoC

Create a new directory BLoC under the lib directory and place all BLoC classes there.

In this directory, create a new file bloc. Dart and add the following code:

abstract class Bloc {
  void dispose();
}
Copy the code

All BLoC classes will follow this interface. There is only one Dispose method on this interface. One thing to keep in mind is that streams must be shut down when they are no longer needed, or they will leak memory. Resources can be examined and released in the Dispose method.

BLoC 1 will manage the app’s location selection feature.

In the BLoC directory, create a new file location_bloc. Dart and add the following code:

class LocationBloc implements Bloc {
  Location _location;
  Location get selectedLocation => _location;

  / / 1
  final _locationController = StreamController<Location>();

  / / 2
  Stream<Location> get locationStream => _locationController.stream;

  / / 3
  void selectLocation(Location location) {
    _location = location;
    _locationController.sink.add(location);
  }

  / / 4
  @override
  voiddispose() { _locationController.close(); }}Copy the code

When importing the base class using Option + Return, select the second option – Import library Package: Restaurant_finder /BLoC/ BLoC. Dart.

Use Option +return for all error prompts until all dependencies are imported correctly.

The LocationBloc mainly achieves the following functions:

  1. Declare a privateStreamControllerAnd manage BLoC’s stream and sink.StreamControllerUse generics to tell the type system what type of object it will send via stream.
  2. This line exposes a public getter method that the caller gets fromStreamControllerThe stream.
  3. The method is the input to BLoC and receives oneLocationModel objects that are cached to private member properties_locationAnd added to the sink of the stream.
  4. Finally, when the BLoC object is released, it is closed in the clearing methodStreamController. Otherwise the IDE will promptStreamControllerA memory leak exists.

So far, the first BLoC has been completed. Now create a BLoC to find the location.

The second BLoC

Create a new file location_query_bloc. Dart in the BLoC directory and add the following code:

class LocationQueryBloc implements Bloc {
  final _controller = StreamController<List<Location>>();
  final _client = ZomatoClient();
  Stream<List<Location>> get locationStream => _controller.stream;

  void submitQuery(String query) async {
    / / 1
    final results = await _client.fetchLocations(query);
    _controller.sink.add(results);
  }

  @override
  voiddispose() { _controller.close(); }}Copy the code

//1 in the code is the BLoC input. This method takes a string parameter and uses the ZomatoClient class in the Start project to get location information from the API. Dart’s async /await syntax makes code more concise. Publish the results to a stream when they return.

This BLoC is almost identical to the last one, except that this BLoC not only stores and reports the location, but also encapsulates an API call.

BLoC is injected into Widget Tree

Now that two blocs have been built, a way is needed to inject them into the Widget tree of Flutter. The use of provider-type Weidget has become the convention of Flutter. A provider is a widget that stores data and provides it to all of its child widgets.

Normally this is what the InheritedWidget does, but since the BLoC object needs to be released, the StatefulWidget will provide the same functionality. The syntax is a bit complicated, but the result is the same.

Create a new file bloc_provider.dart in the BLoC directory and add the following code:

/ / 1
class BlocProvider<T extends Bloc> extends StatefulWidget {
  final Widget child;
  final T bloc;

  const BlocProvider({Key key, @required this.bloc, @required this.child})
      : super(key: key);

  / / 2
  static T of<T extends Bloc>(BuildContext context) {
    final type = _providerType<BlocProvider<T>>();
    final BlocProvider<T> provider = findAncestorWidgetOfExactType(type);
    return provider.bloc;
  }

  / / 3
  static Type _providerType<T>() => T;

  @override
  State createState() => _BlocProviderState();
}

class _BlocProviderState extends State<BlocProvider> {
  / / 4
  @override
  Widget build(BuildContext context) => widget.child;

  / / 5
  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose(); }}Copy the code

The code reads as follows:

  1. BlocProviderIt’s a generic class, genericTIs restricted to an implementationBLoCInterface object. This means that the provider can only store BLoC objects.
  2. ofMethod allows descendant nodes of the Widget Tree to be retrieved using the current Build ContextBlocProvider. This is a very common pattern in Flutter.
  3. This is a common way to get a reference to a generic type.
  4. buildMethod just returns the Widget’s child and does not render anything.
  5. Finally, the provider inherits fromStatefulWidgetThe only reason is the need for accessdisposeMethods. When widgets are removed from the Widget tree, the Flutter calls the Dispose method, which in turn closes the stream.

Interconnection position page

The BLoC layer for locating is now complete and will be used next.

First, in the main.dart file, place a Location BLoC on top of the Material app to store application status. The easiest way to do this is to move the cursor over the MaterialApp, press Option + Return (Alt+Enter on Windows/Linux) and select Wrap with a New Widget from the menu that pops up.

Note: This code snippets are inspired by Didier Boelens’ excellent article Reactive Programming — Streams — BLoC. This widget has not been optimized in any way and could theoretically be improved. For the purposes of this article, we’ll stick with this simple approach, which is perfectly acceptable in most cases. If it is found to be causing performance problems later in the app life cycle, a more comprehensive solution can be found in the Flutter BLoC Package.

Wrap with BlocProvider of type LocationBloc, and create an instance of LocationBloc in the bloc attribute location.

return BlocProvider<LocationBloc>(
  bloc: LocationBloc(),
  child: MaterialApp(
    title: 'Restaurant Finder',
    theme: ThemeData(
      primarySwatch: Colors.red,
    ),
    home: MainScreen(),
  ),
);
Copy the code

Adding widgets on top of the Material App to add data to widgets is a good way to share access to data across multiple pages.

You need to do something similar in the main interface main_screen.dart. Click Option + Return above the LocationScreen widget, this time selecting ‘Wrap with StreamBuilder’. The updated code looks like this:

return StreamBuilder<Location>(
  / / 1
  stream: BlocProvider.of<LocationBloc>(context).locationStream,
  builder: (context, snapshot) {
    final location = snapshot.data;

    / / 2
    if (location == null) {
      return LocationScreen();
    }
    
    // This will be changed this later
    returnContainer(); });Copy the code

StreamBuilder is the secret sauce that makes BLoC Mode so delicious. These widgets will automatically listen for events from the stream. When a new event arrives, the Builder closure function is executed to update the Widget Tree. With StreamBuilder and BLoC mode, there is no need to call the setState() method throughout the tutorial.

In the code above:

  1. forstreamProperty, usingofMethods to obtainLocationBlocAnd add its stream toStreamBuilderIn the.
  2. Initially there is no data in the stream, which is perfectly normal. If there is no data, returnLocationScreen . Otherwise, only an empty container is now returned.

Next, update the location page in location_screen.dart with the LocationQueryBloc you created earlier. Don’t forget to use the widget wrapper tools that come with the IDE to make it easier to update your code.

@override
Widget build(BuildContext context) {
  / / 1
  final bloc = LocationQueryBloc();

  / / 2
  return BlocProvider<LocationQueryBloc>(
    bloc: bloc,
    child: Scaffold(
      appBar: AppBar(title: Text('Where do you want to eat? ')),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(), hintText: 'Enter a location'),
              
              / / 3
              onChanged: (query) => bloc.submitQuery(query),
            ),
          ),
          / / 4
          Expanded(
            child: _buildResults(bloc),
          )
        ],
      ),
    ),
  );
}
Copy the code

In this code:

  1. First, a new one is instantiated at the beginning of the build methodLocationQueryBloc Object.
  2. BLoC is stored inBlocProviderThe BlocProvider manages the life cycle of BLoC.
  3. updateTextFieldonChangedClosure method, passing text toLocationQueryBloc. This triggers the chain of calls to get the data, first calling Zomato, and then sending the returned location information to the stream.
  4. Pass Bloc to_buildResultsMethods.

Add a Boolean field to LocationScreen to keep track of whether the page is a full-screen dialog:

class LocationScreen extends StatelessWidget {
  final bool isFullScreenDialog;
  const LocationScreen({Key key, this.isFullScreenDialog = false})
      : super(key: key); .Copy the code

This Boolean field is simply a flag bit (false by default) that updates the page navigation behavior later when the location information is clicked.

Now update the _buildResults method to add a Stream Builder and display the results in a list. Use ‘Wrap with StreamBuilder’ to quickly update code.

Widget _buildResults(LocationQueryBloc bloc) {
  return StreamBuilder<List<Location>>(
    stream: bloc.locationStream,
    builder: (context, snapshot) {

      / / 1
      final results = snapshot.data;
    
      if (results == null) {
        return Center(child: Text('Enter a location'));
      }
    
      if (results.isEmpty) {
        return Center(child: Text('No Results'));
      }
    
      return_buildSearchResults(results); }); } Widget _buildSearchResults(List<Location> results) {
  / / 2
  return ListView.separated(
    itemCount: results.length,
    separatorBuilder: (BuildContext context, int index) => Divider(),
    itemBuilder: (context, index) {
      final location = results[index];
      return ListTile(
        title: Text(location.title),
        onTap: () {
          / / 3
          final locationBloc = BlocProvider.of<LocationBloc>(context);
          locationBloc.selectLocation(location);

          if(isFullScreenDialog) { Navigator.of(context).pop(); }}); }); }Copy the code

In the code above:

  1. Stream has three conditional branches that return different results. There may be no data, meaning the user has not entered any information; It could be an empty list, meaning Zomato can’t find anything you’re looking for; Finally, it could be a complete list of restaurants, meaning everything is done perfectly.
  2. A list of locations is shown here. The behavior of this method is just plain declarative Flutter code.
  3. inonTapIn the closure, the application retrieves theLocationBlocAnd tells it that the user has selected a location. Clicking on the list item causes the entire screen to temporarily go black.

Continuing to build and run, the application should get location results from Zomato and display them in a list.

Very good! This is real progress.

Restaurant page

The second page of the app will display a list of restaurants based on the results of the search query. It also has its BLoC object, which is used to manage page state.

In the BLoC directory, create a new file restaurant_bloc. Dart and add the following code:

class RestaurantBloc implements Bloc {
  final Location location;
  final _client = ZomatoClient();
  final _controller = StreamController<List<Restaurant>>();

  Stream<List<Restaurant>> get stream => _controller.stream;
  RestaurantBloc(this.location);

  void submitQuery(String query) async {
    final results = await _client.fetchRestaurants(location, query);
    _controller.sink.add(results);
  }

  @override
  voiddispose() { _controller.close(); }}Copy the code

The code is almost the same as the LocationQueryBloc, the only difference being the API and the type of data returned.

Create the file restaurant_screen.dart in the UI directory to use the new BLoC:

class RestaurantScreen extends StatelessWidget {
  final Location location;

  const RestaurantScreen({Key key, @required this.location}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(location.title),
      ),
      body: _buildSearch(context),
    );
  }

  Widget _buildSearch(BuildContext context) {
    final bloc = RestaurantBloc(location);

    return BlocProvider<RestaurantBloc>(
      bloc: bloc,
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: 'What do you want to eat? '),
              onChanged: (query) => bloc.submitQuery(query),
            ),
          ),
          Expanded(
            child: _buildStreamBuilder(bloc),
          )
        ],
      ),
    );
  }

  Widget _buildStreamBuilder(RestaurantBloc bloc) {
    return StreamBuilder(
      stream: bloc.stream,
      builder: (context, snapshot) {
        final results = snapshot.data;

        if (results == null) {
          return Center(child: Text('Enter a restaurant name or cuisine type'));
        }
    
        if (results.isEmpty) {
          return Center(child: Text('No Results'));
        }
    
        return_buildSearchResults(results); }); } Widget _buildSearchResults(List<Restaurant> results) {
    return ListView.separated(
      itemCount: results.length,
      separatorBuilder: (context, index) => Divider(),
      itemBuilder: (context, index) {
        final restaurant = results[index];
        returnRestaurantTile(restaurant: restaurant); }); }}Copy the code

Create a new, separate restaurant_tile.dart file to display the restaurant details:

class RestaurantTile extends StatelessWidget {
  const RestaurantTile({
    Key key,
    @required this.restaurant,
  }) : super(key: key);

  final Restaurant restaurant;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: ImageContainer(width: 50, height: 50, url: restaurant.thumbUrl), title: Text(restaurant.name), trailing: Icon(Icons.keyboard_arrow_right), ); }}Copy the code

The code is very similar to the location page, almost identical. The only difference is that the restaurant is displayed instead of the location.

Modify the MainScreen in the main_screen.dart file to return a restaurant page when the location information is available.

builder: (context, snapshot) {
  final location = snapshot.data;

  if (location == null) {
    return LocationScreen();
  }

  return RestaurantScreen(location: location);
},
Copy the code

Hot Restart this app. Select a location and search for what you want to eat, and a list of restaurants will appear.

It looks delicious. Who’s ready to eat cake?

Collect the restaurant

So far, BLoC mode has been used to manage user input, but there’s more to it than that. Suppose the user wants to track their favorite restaurants and display them in a separate list. This can also be addressed by the BLoC model.

Create a new file for BLoC in the BLoC directory favorite_bloc. Dart to store this list:

class FavoriteBloc implements Bloc {
  var _restaurants = <Restaurant>[];
  List<Restaurant> get favorites => _restaurants;
  / / 1
  final _controller = StreamController<List<Restaurant>>.broadcast();
  Stream<List<Restaurant>> get favoritesStream => _controller.stream;

  void toggleRestaurant(Restaurant restaurant) {
    if (_restaurants.contains(restaurant)) {
      _restaurants.remove(restaurant);
    } else {
      _restaurants.add(restaurant);
    }

    _controller.sink.add(_restaurants);
  }

  @override
  voiddispose() { _controller.close(); }}Copy the code

In // 1, BLoC uses a Broadcast StreamController instead of the regular StreamController. A broadcast stream allows multiple listeners, but a regular stream only allows one. The first two blocs do not need broadcast streams because there is only a one-to-one relationship. For favorites, there are two places to listen to the stream at the same time, so broadcasts are needed here.

Note: As a general rule, when designing BLoC, you should use regular stream in preference, and then change the code to use broadcast stream later when it is found that a broadcast is needed. A Flutter throws an exception when multiple objects attempt to listen on the same regular stream. Think of this as a sign that you need to change your code.

The BLoC needs to be accessed from multiple pages, meaning it needs to be placed above the navigator. Update the main.dart file to add a widget wrapped around the MaterialApp and inside the original provider.

return BlocProvider<LocationBloc>(
  bloc: LocationBloc(),
  child: BlocProvider<FavoriteBloc>(
    bloc: FavoriteBloc(),
    child: MaterialApp(
      title: 'Restaurant Finder',
      theme: ThemeData(
        primarySwatch: Colors.red,
      ),
      home: MainScreen(),
    ),
  ),
);
Copy the code

Next, create a file favorite_screen.dart in the UI directory. This widget will be used to display a list of favorite restaurants:

class FavoriteScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = BlocProvider.of<FavoriteBloc>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Favorites'),
      ),
      body: StreamBuilder<List<Restaurant>>(
        stream: bloc.favoritesStream,
        / / 1
        initialData: bloc.favorites,
        builder: (context, snapshot) {
          / / 2
          List<Restaurant> favorites =
              (snapshot.connectionState == ConnectionState.waiting)
                  ? bloc.favorites
                  : snapshot.data;
    
          if (favorites == null || favorites.isEmpty) {
            return Center(child: Text('No Favorites'));
          }
    
          return ListView.separated(
            itemCount: favorites.length,
            separatorBuilder: (context, index) => Divider(),
            itemBuilder: (context, index) {
              final restaurant = favorites[index];
              returnRestaurantTile(restaurant: restaurant); }); },),); }}Copy the code

In this widget:

  1. Add initialization data toStreamBuilder.StreamBuilderExecution of the Builder closure is triggered immediately, even if there is no data. This allows Flutter to ensure that snapshots always have data, rather than unnecessarily redrawing pages.
  2. Detect the stream’s status and, if no link has been established at this point, replace the new event sent in the stream with an explicit list of favorite restaurants.

Update the Build method of the restaurant page to add an action that adds the favorite restaurant page to the navigation stack when the click event is triggered.

@override
Widget build(BuildContext context) {
  return Scaffold(
      appBar: AppBar(
        title: Text(location.title),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.favorite_border),
            onPressed: () => Navigator.of(context)
                .push(MaterialPageRoute(builder: (_) => FavoriteScreen())),
          )
        ],
      ),
      body: _buildSearch(context),
  );
}
Copy the code

You also need a page to add the restaurant to your favorites.

Create a new file restaurant_details_screen.dart in the UI directory. This page is mostly static layout code:

class RestaurantDetailsScreen extends StatelessWidget {
  final Restaurant restaurant;

  const RestaurantDetailsScreen({Key key, this.restaurant}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Scaffold(
      appBar: AppBar(title: Text(restaurant.name)),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          _buildBanner(),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  restaurant.cuisines,
                  style: textTheme.subtitle.copyWith(fontSize: 18),
                ),
                Text(
                  restaurant.address,
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.w100),
                ),
              ],
            ),
          ),
          _buildDetails(context),
          _buildFavoriteButton(context)
        ],
      ),
    );
  }

  Widget _buildBanner() {
    return ImageContainer(
      height: 200,
      url: restaurant.imageUrl,
    );
  }

  Widget _buildDetails(BuildContext context) {
    final style = TextStyle(fontSize: 16);

    return Padding(
      padding: EdgeInsets.only(left: 10),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Text(
            'Price: ${restaurant.priceDisplay}',
            style: style,
          ),
          SizedBox(width: 40),
          Text(
            'Rating: ${restaurant.rating.average}',
            style: style,
          ),
        ],
      ),
    );
  }

  / / 1
  Widget _buildFavoriteButton(BuildContext context) {
    final bloc = BlocProvider.of<FavoriteBloc>(context);
    return StreamBuilder<List<Restaurant>>(
      stream: bloc.favoritesStream,
      initialData: bloc.favorites,
      builder: (context, snapshot) {
        List<Restaurant> favorites =
            (snapshot.connectionState == ConnectionState.waiting)
                ? bloc.favorites
                : snapshot.data;
        bool isFavorite = favorites.contains(restaurant);

        return FlatButton.icon(
          / / 2
          onPressed: () => bloc.toggleRestaurant(restaurant),
          textColor: isFavorite ? Theme.of(context).accentColor : null,
          icon: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
          label: Text('Favorite')); }); }}Copy the code

In the code above:

  1. This widget uses the Favorites Stream to check if the restaurant is favorites, and then renders the appropriate widget.
  2. FavoriteBlocIn thetoggleRestaurantMethod is implemented so that the UI does not need to care about the state of the restaurant. If the restaurant is not on the favorites list, it will be added; Conversely, if the restaurant is on the favorites list, it will be removed.

Add an onTap closure to the restaurant_tile.dart file to add this new page to your app.

onTap: () {
  Navigator.of(context).push(
    MaterialPageRoute(
      builder: (context) =>
          RestaurantDetailsScreen(restaurant: restaurant),
    ),
  );
},
Copy the code

Build and run the app.

Users should be able to favorites, unfavorites, and view a list of favorites. You can even remove restaurants from the Favorites restaurant page without adding additional code. That’s the power of a stream!

Update location information

What if users want to change the location they are searching for? The current code implementation, if you want to change the location information, must restart the app.

Since the app is already set up to work on a series of streams, adding this feature is a breeze. It could even be as simple as putting a cherry on top of a cake!

Add a Floating Action Button to the restaurant page and display the location page modally:

. body: _buildSearch(context), floatingActionButton: FloatingActionButton( child: Icon(Icons.edit_location), onPressed: () => Navigator.of(context).push(MaterialPageRoute( builder: (context) => LocationScreen(/ / 1
                isFullScreenDialog: true,
              ),
          fullscreenDialog: true)))); }Copy the code

At // 1, set isFullScreenDialog to true. This is what we added to the location page earlier.

We used this flag earlier when we added the onTap closure to the ListTile we wrote for the LocationScreen.

onTap: () {
  final locationBloc = BlocProvider.of<LocationBloc>(context);
  locationBloc.selectLocation(location);
  if(isFullScreenDialog) { Navigator.of(context).pop(); }},Copy the code

The reason for this is that if the location page is rendered modal, it needs to be removed from the navigation stack. Without this code, nothing happens when you click on the ListTile. The stream of location information will be updated, but the UI will not respond.

Build and run the app one last time. You’ll see a Floating Action button that displays the location page modally when clicked.

And then where?

Congratulations on getting the BLoC model. BLoC is a simple but powerful mode that helps you easily tame your app’s state management as it flies up and down on the Widget Tree.

The final sample project can be found in Download Materials for this tutorial. To run the final sample project, add your API key to zomato_client.dart.

Other architectural patterns worth looking at are:

  • The Provider – pub. Dev/packages/pr…
  • Scoped Model – pub. Dev/packages/sc…
  • RxDart – pub. Dev/packages/rx…
  • Story – pub. Dev/packages/re…

Also check out the official stream documentation and the Google IO discussion on BLoC mode.

Hope you enjoyed this Flutter BLoC tutorial. As always, if you have any questions or comments, please feel free to contact me, or comment below!