The original address is here, written by Brian kayfitz.
Here’s a little bit about ide. Many people are mobile, so many people use Android studio. In fact, flutter can also be developed with vs code. I have used both of them. They have their own advantages. Android studio is convenient when there are many directories and files in the initial stage of the project. When refactoring, the modification of the file will be modified together with other file references, and there will be a prompt for deletion. In vs code, you have to manually change the file nameimport
It’s been changed together. However, vs code debugging is much more convenient. However, when debugging the real machine, you should remember firstSelect Device。
text
Designing the architecture of an app is often controversial. Everyone has a cool architecture and a bunch of terms that they like.
Both IOS and Android developers are very familiar with MVC, and use this pattern as the default architecture during development. Model and view are separated, and controller is used as a bridge between them.
However, a set of responsive design brought by flutter is not well compatible with MVC. A new architecture based on this classic pattern appears in the flutter community–BLoC。
Bloc isBusiness Logic CShort for components. The philosophy of bloc is that everything in an app should be considered an event flow: some components subscribe to events, and others respond to events. Bloc manages these sessions in the middle. Dart even built stream into the language itself.
The best thing about this model is that you don’t need to introduce any plug-ins or learn any other syntax. All the content you need is available from flutter.
In this article, we want to create a new app to find restaurants. API hasZomato
Provide. Finally, you will learn the following:
- Wrapping API calls in bloc mode
- Find and display 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 run at the beginningflutter pub get
In the IDE or on the command line. After all the dependencies have been downloaded, you can start coding.
The basic model file and network request file are included in the initial project. It looks like this:
Get the key of the API
Before we start to develop the application, we should first obtain a key of the API we want to use. At zomato’s developer site https://developers.zomato.com/api , register and generate a key.
stayDataLayer
Open the directoryzomato_client.dart
Documents. Modify this constant value:
class ZomatoClient {
final _apiKey = "Your api key here";
}
In actual development, it’s not wise to put key into source code or mix it into version control tools。 It’s just for convenience, not for actual development.
Run it, you will see this effect:
Black, now add code:
Let’s bake a multilayer cake
When writing an app, whether you’re using flutter or other frameworks, it’s critical to layer classes. It’s more like an informal convention, not necessarily in the code.
Each layer, or a set of classes, is responsible for an overall responsibility. There is a directory in the initial projectDataLayer。 This data layer is specially used for model of app and communication with background. It knows nothing about the UI.
Every app is different, but in general, you build something like this:
This architecture convention is not too different from MVC. The UI / router layer can only communicate with the bloc layer, which processes logic and sends events to the data layer and UI. This structure can ensure the smooth expansion of the app when the scale becomes larger.
Go deep into bloc
Bloc is basically based on dart stream.
Stream, like future, is also indart:async
In the bag. A stream is like a future. The difference is that a stream does not return a value asynchronously. A stream can return many values over time. If a future is ultimately a value, then a stream can return a series of values over time.
dart:async
The package provides aStreamController
Class. The flow controller manages two object streams and sinks. A sink corresponds to a stream, which provides data and a sink accepts input values.
In summary, bloc is used to process logic, sink accepts input and stream output.
Positioning interface
Before looking for restaurants, you need to tell zomato where you want to eat. In this section, you’ll create a new simple interface with a search bar and a list to display the search results.
Don’t forget to open it before entering the codeDartFmt。 This is the best way to write a flutter app.
staylib/UITable of contents, one in betweenlocation_screen.dartDocuments. Add oneStatelessWidget
, and 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 positioning interface contains aTextField
The user can enter the location here.
Your ide will report an error if the class you input is not imported. To correct this error, just move the cursor over the identifier and press appleoption+enter(ALT + enter under Windows) or click the red light bulb on the edge. After you click, a menu will appear. Select import and it will be OK.
Add another filemain_screen.dartFile, which will be used to manage the navigation of the interface. Add the following code:
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LocationScreen();
}
}
to updatemain.dartDocument:
MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: MainScreen(),
),
Now run the code like this:
Now it’s time for bloc.
First bloc
staylibCreate aBLoCcatalog. This is used to store all the bloc classes.
Create a new onebloc.dartFile, add the following code:
abstract class Bloc {
void dispose();
}
All bloc classes follow this interface. This interface does nothing but force your code to include adispoose
method. It is very important to turn off the stream when it is not in use, otherwise it will cause memory leakage. Yesdispose
Method, which is called directly by app.
The first bloc will handle the location selected by the app.
stayBLoCDirectory, create a 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+enterImport bloc class.
LocationBloc
It mainly deals with the following matters:
- There’s a private one
StreamController
To manage flows and sinks.StreamController
Use generics to tell the calling code what type of data is returned. - This line uses getters to expose the stream
- This method is used to enter a value for bloc. And the location data is also cached in the
_location
Attribute. - Finally, in the cleaning method
StreamController
The object is closed before it is recycled. If you don’t, your ide will also display errors.
Now that your first bloc is complete, the next step is to find a location.
The second bloc
stayBLoCCreate a new one in the directorylocation_query_bloc.dartFile 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
void dispose() {
_controller.close();
}
}
stay//1This method takes a string parameter and uses theZomatoClient
Class to get location data. It’s used hereasync/await
To make the code look clearer. The result is then pushed into the stream.
This bloc is basically similar to the previous one, except that it also contains an API request.
Combining bloc with component tree
Now that you have two blocs, you need to combine them with the components. This method is basically called flutterprovider。 A provider provides data for this component and its subcomponents.
Generally speaking, this isInheritedWidget
But because bloc needs to be released,StatefulWidget
The same service will be provided. So the syntax is a little more complicated, but the result is the same.
stayBLoCCreate a new onebloc_provider.dartFile 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();
}
}
The above code is parsed as follows:
-
BlocProvider
Is a generic class. typeT
The requirements must be fulfilledBloc
Interface. This means that the provider can only store objects of type bloc. -
of
Method allows the component to get theBlocProvider
。 This is the normal operation of the flutter. - Here is the object to get a generic type
- this
build
Methods don’t build anything - Finally, why does this provider inherit
StatefulWidget
It’s mainly for the sake ofdispose
method. When a component is removed from the tree, flutter callsdispose
Method to close the flow
Combined positioning interface
Now that you have the complete bloc layer code to find the location, it’s time to use it.
First of all, in themain.dartA bloc is used to wrap the material app. The easiest thing is to move the cursor toMaterialApp
Up, downoption+enter(Windows uses Alt + Enter), a menu will pop up, selectWrap with a new widget。
Note: this code was received from Didier boelens https://www.didierboelens.com… —streams—bloc/。 The inspiration. This component has not been optimized, but it can be optimized in theory. This article will continue to use a more initial approach, as this will satisfy most scenarios. If you find performance problems later, you can find improvements in the flutter bloc package.
Then the code goes like this:
return BlocProvider<LocationBloc>(
bloc: LocationBloc(),
child: MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: MainScreen(),
),
);
The simplest way to pass data to the provider is to pass the data to the provider.
staymain_screen.dartDocuments do the same thing. stayLocationScreen.dart
Up and downoption + enter, 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
return Container();
},
);
StreamBuilder
It is the catalyst of bloc mode. These components automatically listen for events in the stream. When a new event is received,builder
Method is executed to update the component tree. useStreamBuilder
And bloc mode is completely unnecessarysetState
The way.
Code analysis:
-
stream
Properties, usingof
MethodLocationBloc
And give the flow toStreamBuilder
。 - At first, there is no data in the stream, which is normal. If there is no data, the app returns
LocationScreen
。 Otherwise, a blank interface will be returned temporarily.
Next, in thelocation_screen.dart
It’s used insideLocationQueryBloc
Update the positioning interface. Don’t forget to use the shortcut keys provided by the IDE to update the 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),
)
],
),
),
);
}
The analysis is as follows:
- First of all, in the
build
Method initializes aLocationQueryBloc
Class. - The bloc is then stored in the
BlocProvider
inside - to update
TextField
OfonChange
Method, the modified text is submitted to theLocationQueryBloc
Object. This initiates the request API and returns the chain reaction of the data. - Passing the bloc object to
_buildResult
method.
toLocationScreen
Add a bool member at a time to mark whether it is a full screen dialog.
class LocationScreen extends StatelessWidget {
final bool isFullScreenDialog;
const LocationScreen({Key key, this.isFullScreenDialog = false})
: super(key: key);
...
This bool is just a simple tag. It will be used when selecting a location in the future.
Update now_buildResults
method. Add a stream builder to display the results in a list. You can use itWrap with StreamBuilderTo 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();
}
},
);
},
);
}
The code is parsed as follows:
- Stream can return three kinds of results: no data (the user did nothing), and an empty array, that is, zomato did not find a qualified result. Finally, a list of restaurants.
- Show a set of data returned. This is also the normal operation of the router
-
onTap
Method, users click on a restaurant to obtainLocationBloc
And jump back to the previous page
Run the code again. You can see this effect:
There’s a little bit of progress.
Restaurant page
The second page of the app displays a group of restaurants based on the search results. It also has a corresponding bloc object to manage state.
stayBLoCDirectory create a new filerestaurant_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
void dispose() {
_controller.close();
}
}
andLocationQueryBloc
The base class is similar. The only difference is the type of data returned.
Now inUICreate a new one in the directoryrestaurant_screen.dartFile. The new bloc is put into use
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 another onerestaurant_tile.dartTo 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),
);
}
}
This code looks very similar to the location interface code. The only difference is that it shows the restaurant, not the location.
modifymain_screen.dartinMainScreen
Code for:
builder: (context, snapshot) {
final location = snapshot.data;
if (location == null) {
return LocationScreen();
}
return RestaurantScreen(location: location);
},
After you select a location, a list of restaurants will be displayed.
Favorite restaurant
So far, bloc has only been used to process user input. It can do more than that. Suppose users want to record their favorite restaurants and display them on a separate list page. This can also be solved by using the bloc mode.
stayBLoCDirectory, create a new onefavorite_bloc.dartFile 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
void dispose() {
_controller.close();
}
}
Code parsing: in// 1
We use aBroadcastOfStreamController
, rather than a regular oneStreamController
。BroadcastA stream of type can have multiple listeners, while a regular stream only allows one. In the first two blocs, there is only a one-to-one relationship, so there is no need for multiple listeners. For the favorite function, two places are needed to monitor, so broadcasting is a must.
Note: the general rule of using bloc is to use the regular stream first, and then refactor the code when broadcasting is needed. If more than one object is listening to the same regular stream, then flutter throws an exception. Use this as a sign that code needs to be refactored.
This bloc needs to be accessible to multiple pages, which means it should be placed outside the navigator. to updatemain.dartAdd 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(),
),
),
);
Next, in theUIAdd afavorite_screen.dartDocuments. This component displays the user’s favorite restaurant:
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 component:
- stay
StreamBuilder
Add the initial data.StreamBuilder
The builder method is called immediately, even if there is no data. - Check the connection status of the app.
Next, update thebuild
Method: 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),
);
}
You need another interface where users can set the restaurant as their favorite.
stayUINew under directoryrestaurant_details_screen.dartDocuments. The main codes are 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'),
);
},
);
}
}
Code analysis:
- This component uses the
FavoriteBloc
To determine whether a restaurant is a favorite restaurant, and the corresponding update interface -
FavoriteBloc#toggleRestaurant
Method allows the component not to care about whether a restaurant is a favorite.
stayrestaurant_tile.dartDocument’sonTap
Method to add the following code:
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
RestaurantDetailsScreen(restaurant: restaurant),
),
);
},
Run code:
Update positioning
What if users want to update the location they’re looking for? Now if you change the location, the app will have to restart.
Because you’ve made the code work on a set of data that is passed by the stream, adding a function becomes very simple, just like putting a cherry on a cake.
On the restaurant page, add a floating button. Click this button and the location page will pop up.
...
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// 1
,isFullScreenDialog
Set to true so that the navigation page will be displayed in full screen after it pops up.
stayLocationScreen
OfLisTile#onTap
This is how the method is usedisFullScreenDialog
Of:
onTap: () {
final locationBloc = BlocProvider.of<LocationBloc>(context);
locationBloc.selectLocation(location);
if (isFullScreenDialog) {
Navigator.of(context).pop();
}
},
This is done in order to remove the location when it is also displayed as a dialog box.
Run the code again, you will see a floating button, click it will pop up the location page.
last
Congratulations on having learned the bloc mode. Bloc is a simple and powerful app state management mode.
You can download the final project code in this case. If you want to run it, remember to get an app key from zomato and update itzomato_client.dartCode (don’t put it in code version control, such as GitHub, etc.). Other modes that can be seen:
- Provider:https://pub.dev/packages/prov…
- Scoped Model:https://pub.dev/packages/prov…
- RxDart:https://pub.dev/packages/prov…
- Redux:https://pub.dev/packages/prov…
You can also view official documents, or Google IO videos.
I hope you like this blog tutorial and leave any questions in the comments section.