The original address is here. The author is Brian Kayfitz.

One thing about IDE is that a lot of people are mobile, so there are a lot of people using Android Studio. Flutter can also be developed using VS Code. I’ve used both, and they both have their advantages. Android Studio was convenient for directories and files early in the project. Changes made to files during refactoring will be added to other file references, and deletion will be prompted. These are not available in VS Code, so you need to manually change the file name and import as well. However, VS Code debugging is much easier. However, remember to Select Device first when debugging the real machine.

The body of the

Designing an app’s architecture is often controversial. Everyone has their favorite cool architecture and a bunch of nouns.

IOS and Android developers are very familiar with MVC and use this pattern as the default architecture when developing. The Model and View are separated, and the Controller acts as a bridge between them.

However, the responsive design of Flutter is not compatible with MVC. A new structure derived from this classic model has emerged in the Community of Flutter, BLoC.

BLoC stands for Business Logic Components. BLoC’s philosophy is that everything in an app should be considered a stream of events: some components subscribe to events and others respond to them. BLoC centrally manages these sessions. Dart even builds streams into the language itself.

The best part about this mode is that you don’t need to import any plug-ins or learn any other syntax. All the content that Flutter needs is provided.

In this article, we will create an app to find restaurants. The API is provided by Zomato. In the end you will learn the following:

  • Package API calls in BLoC mode
  • Find and display the results asynchronously
  • Maintain a list of favorite restaurants that can be accessed from multiple pages

start

Download the start project code here and open it with your favorite IDE. Remember to start the flutter pub get, either in the IDE or on the command line. After all dependencies have been downloaded, you can start coding.

The basic Model file and network request file are included in the start project. It looks like this:

Get the API Key

Before we start developing our application, we first need to get a key for the API we want to use. Registered in site developers.zomato.com/api Zomato developer, and generate a key.

In the DataLayer directory, open the zomato_client.dart file. Change this constant value:

class ZomatoClient {
  final _apiKey = "Your api key here";
}
Copy the code

Putting keys in source code or incorporating them into a version control tool is not a good idea in real development. This is for convenience only and should not be used in actual development.

When run, you should see something like this:

Black, now add code:

Let’s bake a tiered cake

When writing apps, whether you use a Flutter or any other framework, layering classes is crucial. It’s more of an informal convention and doesn’t have to be reflected in the code.

Each layer, or set of classes, is responsible for an overall responsibility. There was a directory, DataLayer, in the initial project. This data layer is specifically responsible for the app’s model and communication with the background. It doesn’t know anything about the UI.

Every app is different, but in general you build something like this:

This architectural convention is not too different from MVC. The UI/Flutter layer can only communicate with the BLoC layer, which processes logic and sends events to the data layer and the UI. This structure ensures smooth expansion of the APP as it grows in size.

In-depth BLoC

BLoC is basically based on the Dart Stream.

Streams, like futures, are in the Dart: Async package. A stream is like a future, except that streams don’t just return one value asynchronously; streams can return many values over time. If a future is ultimately a value, then a stream will return a series of values over time.

The Dart: Async package provides a StreamController class. The flow controller manages the two objects flow and slot (sink). Sink corresponds to the flow. The flow provides data and sink accepts input values.

To sum up, BLoC is used to process logic, sink receives input, stream output.

Positioning interface

Before looking for restaurants, you tell Zomato where you’re going to eat. In this section, you will create a simple interface with a search bar and a list of search results.

Don’t forget to open DartFmt before entering the code. This is the way to compose the code for a Flutter app.

Dart is a location_screen.dart file in the lib/UI directory. Add a StatelessWidget and name it 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 interface contains a TextField where the user can enter a location.

Your IDE will get an error if the class you entered is not imported. To correct this, simply move the cursor over the identifier and press Option +Enter on MAC (Alt+Enter on Windows) or tap the small red light on the side. After clicking on it, a menu will appear. Select the “Import” one and it will be OK.

Add another file, main_screen.dart, which will be used to manage navigation on the interface. Add the following code:

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

Update the main.dart file:

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

Now run the code like this:

It’s BLoC time now.

The first BLoC

Create a BLoC directory in the lib directory. This is where all the BLoC classes are stored.

Create a new bloc. Dart file and add the following code:

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

All BLoC classes follow this interface. This interface does nothing but force your code to include a dispoose method. It is important to turn streams off when they are not in use, otherwise they will cause a memory leak. With dispose method, APP will call it directly.

The first BLoC will process the location chosen by the APP.

In the BLoC directory, create a new file location_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
  voiddispose() { _locationController.close(); }}Copy the code

Use the option+ Enter import Bloc class.

The LocationBloc is mainly dealing with:

  1. There’s a private oneStreamControllerTo manage flows and sinks.StreamControllerUse generics to tell the calling code what type of data to return.
  2. This line uses the getter to expose the stream
  3. This method is used to input values for BLoC. And the location data is cached_locationIn the properties.
  4. And finally, in the cleanup methodStreamControllerThe object is closed before it is reclaimed. If you don’t, your IDE will also display errors.

Now that your first BLoC is done, it’s time to find the location.

The second BLoC

Create a new location_query_bloc. Dart file 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

At **//1**, this method takes a string argument and uses the ZomatoClient class to get the location data. Async /await is used here to make the code look cleaner. The results are then pushed into the stream.

This BLoC is basically similar to the last one, except this one also contains an API request.

BLoC and the component tree

There are already two blocs, and you need to combine them with the components. This method of Flutter is basically called a provider. A provider provides data to this component and its children.

Normally this is what the InheritedWidget component does, but because BLoC needs to be released, the StatefulWidget provides the same service. So the syntax is a little bit more complicated, but the result is the same.

Create a new bloc_provider.dart file in BLoC 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 = context.ancestorWidgetOfExactType(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 above is parsed as follows:

  1. BlocProviderIs a generic class. typeTThe requirement must be fulfilledBlocInterface. This means that the provider can only store objects of the type BLoC.
  2. ofMethod allows a component to retrieve the component tree from the current contextBlocProvider. This is the normal operation of Flutter.
  3. Here is an object that gets a generic type
  4. thisbuildMethod doesn’t build anything
  5. Finally, why should this provider inheritStatefulWidgetWell, mostly fordisposeMethods. A Flutter is called when a component is removed from the treedisposeMethod to close the flow

Combined positioning interface

You already have the complete BLoC layer code to find your location, so it’s time to use it.

First, wrap the Material app with a BLoC in main.dart. The simplest is to move the cursor over the MaterialApp and press Option + Enter (Alt + Enter for Windows). This will bring up a menu and select Wrap with a New Widget.

Note: this code is received Didier Boelens https://www.didierboelens.com/2018/08/reactive-programming – streams – bloc /. The inspiration. This component is not optimized yet, but in theory it could be. This article will continue to use a more initial approach, as this will suffice for most scenarios. If you find performance problems later, you can find improvements in the Flutter BLoC package.

Then the code looks like this:

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

Putting a provider layer around the Material App is the easiest way to pass data to components that need it.

Do something similar in the main_screen.dart file. Dart press Option + Enter and select **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 catalyst for BLoC mode. These components automatically listen for events on the stream. When a new event is received, the Builder method executes, updating the component tree. Using StreamBuilder and BLoC mode eliminates the need for setState methods at all.

Code parsing:

  1. streamProperty, usingofMethods to obtainLocationBlocAnd give the stream toStreamBuilder.
  2. The stream starts with no data, which is normal. App returns if there isn’t any dataLocationScreen. Otherwise, a blank screen is temporarily returned.

Next, update the location screen with the LocationQueryBloc in location_screen.dart. Don’t forget to use the IDE shortcuts 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

The analysis is as follows:

  1. First, inbuildThe method initializes one at the beginningLocationQueryBlocClass.
  2. BLoC was then storedBlocProviderinside
  3. updateTextFieldtheonChangeMethod, where the modified text is submitted toLocationQueryBlocObject. This starts the chain reaction of requesting the API and returning the data.
  4. Pass the bloc object to_buildResultMethods.

Add a bool to LocationScreen once to indicate whether it 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 bool is just a simple notation. We’re going to use it when we select a location.

Now update the _buildResults method. Add a Stream Builder to display the results in a list. You can 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

Code parsing is as follows:

  1. Stream can return three results: no data (the user did nothing), and an empty array, which means Zomato did not find a result that matches the criteria. Finally, a list of restaurants.
  2. Shows a set of data returned. This is also the normal operation of flutter
  3. onTapMethod, the user clicks on a restaurant and gets itLocationBlocAnd jump back to the previous page

Run the code again. You’ll see something like this:

We’re finally making some progress.

Restaurant page

The second page of the app displays a list of restaurants based on the results of the search. It also has a corresponding BLoC object to manage the state.

Create a new file restaurant_bloc. Dart in the BLoC directory. 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

Similar to the LocationQueryBloc group. The only difference is the type of data returned.

Now create a new restaurant_screen.dart file in the UI directory. And put the new BLoC into service:

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

Also create a new restaurant_tile.dart file to show the details of the restaurant:

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

This code looks very similar to the location interface code. The only difference is that it shows the restaurant instead of the location.

Alter main_screen.dart MainScreen:

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

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

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

After you select a location, a list of restaurants will be displayed.

A favorite restaurant

BLoC has so far only been used to process user input. It can do more than that. Suppose the user wants to record their favorite restaurants and display them on a separate list page. This can also be addressed by the BLoC model.

In the BLoC directory, create a new favorite_bloc. Dart file to store the 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 the // 1 section, a Broadcast StreamController is used instead of a regular StreamController. A Broadcast stream can have multiple listeners, whereas a Broadcast stream is allowed to have only one listener. There is only a one-to-one relationship in the first two BLoC, so there is no need for multiple monitors. For this favorite feature, you need two places to listen, so broadcasting is a must.

Note: The general rule for BLoC is to use the regular stream first and then refactor the code later if it needs to be broadcast. A Flutter throws an exception if multiple objects listen on the same regular stream. Use this as a sign that you need to refactor your code.

BLoC needs to be accessible from multiple pages, which means it needs to be placed outside the navigator. Update main.dart to add the following components:

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, add a favorite_screen.dart file in the UI directory. This component displays the user’s 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 component:

  1. inStreamBuilderAdd initial data to.StreamBuilderThe Builder method is called immediately, even if there is no data.
  2. Check the app connection status.

Next update the build method of the restaurant interface and add your favorite restaurant to the navigation:

@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 need another interface where the user can set the restaurant as their favorite.

Create a new restaurant_details_screen.dart file in the UI directory. The main code is as follows:

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

Code parsing:

  1. This component is usedFavoriteBlocTo determine whether a restaurant is a favorite restaurant, and the corresponding update interface
  2. FavoriteBloc#toggleRestaurantMethod to keep the component from caring about whether a particular restaurant is a favorite.

Add the following code to the restaurant_tile.dart file’s onTap method:

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

Run code:

Update the location

What if the user wants to update the location they’re looking for? Now if you change the location, the app has to restart.

Because you’ve got the code working on a set of data passed by the stream, adding a function is as easy as putting a cherry on top of a cake.

On the Restaurant page, add a float button. Clicking this button will bring up the location page.

. 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, isFullScreenDialog is set to true so that the location page is displayed full screen when it pops up.

The listileontap method on the LocationScreen uses isFullScreenDialog like this:

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

This is done so that the location can be removed when it is also displayed as a dialog.

Run the code again and you will see a float button that will bring up the location page.

The last

Congratulations on learning the BLoC pattern. BLoC is a simple and powerful app state management mode.

You can download the final project code in this example. To run, always get an app key from Zomato and update the Zomato_client. dart code (don’t put it in code version control, github, etc.). Other patterns to look at:

  • The Provider: pub. Dev/packages/pr…
  • Scoped Model: pub. Dev/packages/pr…
  • RxDart: pub. Dev/packages/pr…
  • Story: pub. Dev/packages/pr…

You can also check out the official documentation, or Google IO videos.

Hope you enjoyed the BLoC tutorial and leave any questions in the comments section.