Fluent: introduction to bloc mode

Time:2022-5-5

Original text:Fluent: introduction to bloc mode

Learn how to use the popular bloc pattern to build a fluent application and use dart streams to manage the data flow through widgets.

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

IOS and Android developers are proficient in model view controller (MVC) and use it as the default choice for building applications. Model and view are separated, and the controller is responsible for sending signals between them.

However, flutter brings a new responsive style, which is not fully compatible with MVC. A variation of this classic pattern has appeared in the flutter community – that isBLoC

Bloc representativeBusiness Logic Components。 The main idea of bloc is that all contents in the app should be represented as event flow: some widgets send events; Other widgets respond. Bloc is in the middle and manages these sessions. Dart even provides syntax for processing streams, which have been integrated into the language.

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

In this tutorial, you will create an app that usesZomatoFind restaurants using the API provided. At the end of the tutorial, the app will do the following:

  1. Encapsulating API calls using bloc mode
  2. Search restaurants and display results asynchronously
  3. Maintain the collection list and display it on multiple pages

Ready to start

Download and open with your favorite idestarterProject engineering. This tutorial will useAndroid Studio, if you like to use Visual Studio code, you can. Make sure to run at the command line or IDE promptflutter packages getTo download the latest version of the HTTP package.

This starter project contains some basic data models and network files. When opening a project, it should be as shown in the following figure:

Fluent: introduction to bloc mode

Here are three files to communicate with zomato.

Get zomato API key

Before building an app, you need to get an API key. Jump to zomato developer pagehttps://developers.zomato.com…, create an account and generate a new key.

openDataLayerDirectoryzomato_client.dart, modify the constants in the class declaration:

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

Note:The best practice for product 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, which is imported from other places when building the app.

Build and run the project, which will display a blank interface.

Fluent: introduction to bloc mode

Nothing exciting, is it? It’s time to change it.

Let’s bake a sandwich cake

When writing applications, it is very important to organize classes hierarchically, whether using fluent or any other framework. This is more like an informal agreement; It’s not something you can see in code.

Each layer, or group of classes, is responsible for a specific task. The starter project has one namedDataLayerThe data layer is responsible for the data model of the application and communication with the back-end server, but it knows nothing about the UI.

Each project is slightly different, but in general, the general structure is basically as follows:

Fluent: introduction to bloc mode

This architecture convention is not much different from the classic MVC. The UI / fluent layer can only communicate with the bloc layer. The bloc layer sends events to the data layer and UI layer, and processes business logic at the same time. With the continuous growth of application functions, this structure can be well extended.

In depth analysis of bloc

Fluent: introduction to bloc mode

Stream, like future, is also composed ofdart:asyncPackage provided. Streams are similar to future, except that future returns a value asynchronously, but streams can produce multiple values over time. If future is a value that will eventually be provided, the stream is a series of values that are provided sporadically over time.

dart:asyncThe package provides a nameStreamControllerObject. Streamcontroller is a manager object that instantiates streams and sink. Sink is the opposite of stream. Stream continuously generates output and sink continuously receives input.

In summary, blocs are entities that process and store business logic, receive input data using sinks, and provide data output using stream.

Location page

Before using the app to find a suitable place to eat, you need to tell zomato where you want to eat. In this chapter, you will create a simple page with a header search area and a list of search results.

Note:Don’t forget to open dartfmt before entering these code examples. It is the only way to maintain the code style of the fluent application.

In Engineeringlib/UICreate a directory namedlocation_screen.dartNew file. Add a to the fileStatelessWidgetAn extension class namedLocationScreen

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'));
  }
 }

The location page contains aTextField, users can enter geographic location information here.

Note:When you enter classes, the IDE will prompt an error because these classes are not imported. To fix this problem, move the cursor over any red underlined symbol, and then press on MacOSoption+enter(on Windows / Linux, pressAlt+Enter)Or click the red light bulb. A menu will pop up, in which you can select the correct file to import.

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

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LocationScreen();
  }
}

Finally, updatemain.dartTo return to the new page.

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

Build and run the app. It should look like this:

Fluent: introduction to bloc mode

Although it is better than before, it still can’t do anything. It’s time to create some blocs.

First bloc

staylibCreate a new directory under directoryBLoCPut all classes here.

Create a new file in this directorybloc.dart, and add the following code:

abstract class Bloc {
  void dispose();
}

All bloc classes will follow this interface. There is only one in this interfacedisposemethod. One thing to keep in mind is that when a stream is no longer needed, it must be turned off, otherwise a memory leak will occur. Can be indisposeMethod.

The first bloc will be responsible for managing the location selection function of the app.

stayBLoCDirectory, new filelocation_bloc.dart, 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
  void dispose() {
    _locationController.close();
  }
}

useoption+returnWhen importing the base class, select the second option-Import library package:restaurant_finder/BLoC/bloc.dart

Fluent: introduction to bloc mode

Use for all error promptsoption+returnUntil all dependencies are imported correctly.

LocationBlocIt mainly realizes the following functions:

  1. Declared a privateStreamController, manage the stream and sink of bloc.StreamControllerUse generics to tell the type system what type of object it will send through stream.
  2. This line exposes a public getter method through which the caller obtainsStreamControllerStream.
  3. This method is the input of bloc and receives aLocationModel object and cache it 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 cleanup methodStreamController。 Otherwise, the IDE will promptStreamControllerThere is a memory leak.

So far, the first bloc has been completed. Next, create a bloc to find the location.

Second bloc

stayBLoCNew file in directorylocation\_query\_bloc.dart, 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
  void dispose() {
    _controller.close();
  }
}

In code//1At, it is the input of bloc. This method receives a string type parameter and uses the parameter in the start projectZomatoClientClass gets location information from the API. DART’s ` Async
/Await ` syntax can make the code more concise. After the result is returned, it is published to the stream.

This bloc is almost the same as the previous one, except that it not only stores and reports the location, but also encapsulates an API call.

Inject bloc into widget tree

Now that we have established two blocs, we need a way to inject them into the widget tree of fluent. useproviderType of gadget has become the Convention of flutter. A provider is a widget that stores data. It can provide data to all its child widgets.

Usually this isInheritedWidgetBut because the bloc object needs to be released,StatefulWidgetThe same functionality will be provided. Although the syntax is a little complicated, the result is the same.

stayBLoCNew file under directorybloc_provider.dart, 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();
  }
}

The code is interpreted as follows:

  1. BlocProviderIs a generic class, genericTIs limited to one implementationBLoCObject of the interface. This means that this provider can only store bloc objects.
  2. ofMethod allows the descendant nodes of the widget tree to use the current build context to retrieveBlocProvider。 This is a very common pattern in fluent.
  3. This is a common way to get generic type references.
  4. buildMethod just returns the child of the widget and does not render anything.
  5. Finally, this provider inherits fromStatefulWidgetThe only reason is that access is requireddisposemethod. When the widget is removed from the widget tree, fluent will call the dispose method, which will close the flow in turn.

Docking location page

Now that you have completed the bloc layer for finding locations, you will use it next.

Preferred, inmain.dartIn the file, a location bloc is placed on the upper layer of the material app to store the application status. The easiest way is to move the cursor toMaterialAppUp, pressoption+return(yes on Windows / Linux)Alt+Enter), select from the pop-up menuWrap with a new widget

Note:This code snippet is inspired by this wonderful article by Didier boelensReactive Programming — Streams — BLoC。 This widget has not been optimized and can be improved in theory. For the purposes of this article, we still use this simple method, which is completely acceptable in most cases. If you find that it causes performance problems later in the app life cycle, you canFlutter BLoC PackageFind a more comprehensive solution.

useLocationBlocTypeBlocProviderPackage andblocCreate a property locationLocationBlocexample.

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

Add widgets on the upper layer of the material app and add data in the widget, which is a good way to share and access data on multiple pages.

In the main interfacemain_screen.dartSimilar things need to be done in. stayLocationScreen Click above the widgetoption+return, this choice‘Wrap with StreamBuilder’。 The updated code is as follows:

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
    return Container();
  },
);

StreamBuilderIt’s a 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 will be executed to update the widget tree. useStreamBuilderAnd bloc mode, you don’t need to call the setstate () method in the whole tutorial.

In the code above:

  1. aboutstreamProperties, usingofMethod acquisitionLocationBlocAnd add its stream toStreamBuilderYes.
  2. At first, there is no data in the stream, which is completely normal. If there is no data, returnLocationScreen 。 Otherwise, only one empty container is now returned.

Next, use the previously createdLocationQueryBlocto updatelocation_screen.dartThe location page in. Don’t forget to use the widget wrapper provided by the IDE to update the code more easily.

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

In this Code:

  1. First, a new one is instantiated at the beginning of the build methodLocationQueryBloc Object.
  2. Store bloc inBlocProviderIn, the blocprovider will manage the lifecycle of bloc.
  3. to updateTextFieldofonChangedClosure method, passing text toLocationQueryBloc。 This will trigger the call chain to get the data, first call zomato, and then send the returned location information to the stream.
  4. Pass to bloc_buildResultsmethod.

stayLocationScreenAdd a Boolean field to track whether this page is a full screen dialog box:

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

This Boolean field is just a simple flag bit (the default value is false), which is used to update the page navigation behavior when clicking the location information later.

Update now_buildResultsMethod, add a stream builder and display the results in a list. use‘Wrap with StreamBuilder’Quickly update the 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();
          }
        },
      );
    },
  );
}

In the code above:

  1. Stream has three conditional branches that return different results. There may be no data, which means that the user has not entered any information; It may be an empty list, which means zomato can’t find anything you want to find; Finally, it may be a complete list of restaurants, which means everything is done perfectly.
  2. The location information is displayed here. The behavior of this method is ordinary declarative fluent code.
  3. stayonTapIn closures, the application retrieves the root of the treeLocationBlocAnd tell it that the user has selected a location. Clicking on a list item will cause the entire screen to darken temporarily.

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

Fluent: introduction to bloc mode

Very good! This is real progress.

Restaurant page

The second page of the app will display the list of restaurants according to the results of the search query. It also has its own bloc object to manage the page state.

stayBLoCNew file under directoryrestaurant_bloc.dart, 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
  void dispose() {
    _controller.close();
  }
}

The code is almost the same asLocationQueryBlocThe only difference is the API and the data type returned.

stayUICreate file in directoryrestaurant_screen.dartTo 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];
        return RestaurantTile(restaurant: restaurant);
      },
    );
  }
}

Create a new independentrestaurant_tile.dartFile to display 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),
    );
  }
}

The code and location page are very similar, almost the same. The only difference is that the restaurant is displayed here, not the location information.

modifymain_screen.dartIn the fileMainScreen, return to a restaurant page after getting the location information.

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

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

  return RestaurantScreen(location: location);
},

Hot restart this app. Select a location and search for what you want to eat. A list of restaurants will appear in front of you.

Fluent: introduction to bloc mode

It looks delicious. Who’s going to eat the cake?

Collection Restaurant

So far, bloc mode has been used to manage user input, but it is much more than that. Suppose users want to track their favorite restaurants and display them in a separate list. This can also be solved by bloc mode.

stayBLoCCreate a new file for bloc under directoryfavorite_bloc.dartTo 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
  void dispose() {
    _controller.close();
  }
}

stay// 1Here, bloc uses aBroadcast StreamControllerInstead of conventionalStreamController。 Broadcast stream allows multiple listeners, but conventional stream only allows one. The first two blocs do not need broadcast streams because there is only one-to-one relationship. For the collection function, two places need to monitor the stream at the same time, so broadcasting is needed here.

Note:As a general rule, when designing bloc, we should give priority to using conventional stream. When it is found that broadcasting is needed later, we can modify the code to use broadcast stream. When multiple objects try to listen to the same regular stream, fluent will throw an exception. You can think of this as a sign that you need to modify your code.

This bloc needs to be accessed from multiple pages, which means it needs to be placed above the navigator. to updatemain.dartFile, add a widget and wrap it inMaterialAppOutside 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(),
    ),
  ),
);

Next inUINew file under directoryfavorite_screen.dart。 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];
              return RestaurantTile(restaurant: restaurant);
            },
          );
        },
      ),
    );
  }
}

In this widget:

  1. Add initialization data toStreamBuilderStreamBuilderThe execution of the builder closure will be triggered immediately, even if there is no data. This allows flutter to ensure that the snapshot always has data, rather than redrawing the page unnecessarily.
  2. Detect the status of the stream. If the link has not been established at this time, use an explicit list of favorite restaurants to replace the new events sent in the stream.

Update restaurant pagebuildMethod, add an action to add 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),
  );
}

You also need a page to add restaurants to your favorite restaurants.

stayUINew file under directoryrestaurant\_details\_screen.dart。 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'),
        );
      },
    );
  }
}

In the above code:

  1. This widget uses the collection stream to detect whether the restaurant has been collected, and then renders the appropriate widget.
  2. FavoriteBlocMediumtoggleRestaurantMethod, so that the UI does not need to care about the state of the restaurant. If the restaurant is not in the collection list, it will be added; Conversely, if the restaurant is in the favorites list, it will be deleted.

stayrestaurant_tile.dartAdd to fileonTapClosure, which is used to add this new page to the app.

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

Build and run the app.

Users should be able to collect, cancel and view the collection list. You can even delete restaurants from your favorite restaurants page without adding additional code. This is the power of stream!

Fluent: introduction to bloc mode

Update location information

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

Because the work of the app has been set to be based on a series of streams, adding this function is almost effortless. It’s even as simple as putting a cherry on a cake!

Add a floating action button on the restaurant page and display the location page in a modal way:

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

stay// 1Place, setisFullScreenDialogThe value of istrue。 This is what we added to the location page before.

Before forLocationScreenWrittenListTileIn, addonTapThis flag has been used in closures.

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

The reason for this is that if the location page is presented in a modal way, it needs to be removed from the navigation stack. If you don’t have this code, when you clickListTileNothing will happen. The location information stream will be updated, but the UI will not respond.

Build and run the app for the last time. You will see a floating action button. When you click this button, the location page will be displayed in a modal way.

Fluent: introduction to bloc mode

Then where?

Congratulations on mastering bloc mode. Bloc is a simple but powerful model that can help you easily tame the state management of apps because it can fly up and down the widget tree.

You can find it in theDownload MaterialsFind the final sample project in. If you want to run the final sample project, you need to add your API key tozomato_client.dart

Other architecture models worth seeing are:

Please also checkOfficial document of stream, and about bloc modeGoogle IO discussion

I hope you enjoy this flutter bloc tutorial. As usual, if you have any questions or comments, please feel free to contact me or comment below!