共计 17571 个字符,预计需要花费 44 分钟才能阅读完成。
原文:Getting Started with the BLoC Pattern
作者:Brian Kayfitz
理解如何应用风行的 BLoC 模式来构建 Flutter 应用程序,并应用 Dart streams 治理通过 Widgets 的数据流。
设计应用程序的构造通常是利用程序开发中争执最强烈的话题之一。每个人仿佛都有他们最喜爱的、带有花哨首字母缩略词的架构模式。
iOS 和 Android 开发人员精通 Model-View-Controller(MVC),并将其作为构建应用程序的默认抉择。Model 和 View 是离开的,Controller 负责在它们之间发送信号。
然而,Flutter 带来了一种新的响应式格调,其与 MVC 并不齐全兼容。这个经典模式的一个变体曾经呈现在了 Flutter 社区 – 那就是 BLoC。
BLoC 代表 Business Logic Components。BLoC 的宗旨是 app 中的所有内容都应该体现为事件流:局部 widgets 发送事件;其余的 widgets 进行响应。BloC 位于两头,治理这些会话。Dart 甚至提供了解决流的语法,这些语法曾经融入到了语言中。
这种模式最好的中央是不须要导入任何插件,也不须要学习任何自定义语法。Flutter 自身曾经蕴含了你须要的所有货色。
在本教程里,你将创立一个 app,应用 Zomato 提供的 API 查找餐厅。在教程的结尾,这个 app 将实现上面的事件:
- 应用 BLoC 模式封装 API 调用
- 搜寻餐厅并异步显示后果
- 保护珍藏列表,并在多个页面展现
筹备开始
下载并应用你最喜爱的 IDE 关上 starter 我的项目工程。本教程将应用 Android Studio,如果你喜爱应用 Visual Studio Code 也齐全能够。确保在命令行或 IDE 提醒时运行 flutter packages get
,以便下载最新版本的 http 包。
这个 starter 我的项目工程蕴含一些根底的数据模型和网络文件。关上我的项目时,应该如下图所示:
这里有 3 个文件用来和 Zomato 通信。
获取 Zomato API Key
在开始构建 app 之前,须要获取一个 API key。跳转到 Zomato 开发者页面 https://developers.zomato.com…,创立一个账号,并产生一个新的 key。
关上 DataLayer 目录下的 zomato_client.dart,批改类申明中的常量:
class ZomatoClient {
final _apiKey = 'PASTE YOUR API KEY HERE';
...
Note: 产品级 app 的最佳实际是,不要将 API key 存储在源码或 VCS(版本控制系统)中。最好是从一个配置文件中读取,配置文件在构建 app 时从其余中央引入。
构建并运行这个工程,它将显示一个空白的界面。
没有什么让人兴奋的,不是吗?是时候扭转它了。
让咱们烤一个夹心蛋糕
在写应用程序的时候,将类分层进行组织是十分重要的,无论是应用 Flutter 还是应用其余的什么框架。这更像是一种非正式的约定;并不是能够在代码中看到的具象的货色。
每一层,或者一组类,负责一个具体的工作。starter 工程中有一个命名为 DataLayer 的目录,这个数据层负责应用程序的数据模型和与后端服务器的通信,但它对 UI 无所不知。
每个我的项目工程都有轻微的不同,但总的来说,大体构造根本如下所示:
这种架构约定与经典的 MVC 并没有太大的不同。UI/Flutter 层只能与 BLoC 层通信。BLoC 层发送事件给数据层和 UI 层,同时解决业务逻辑。随着应用程序性能的一直增长,这种构造可能很好的进行扩大。
深刻分析 BLoC
流(stream),和 Future 一样,也是由 dart:async
包提供。流相似 Future,不同的是,Future 异步返回一个值,但流能够随着工夫的推移生产多个值。如果 Future 是一个最终将被提供的值,那么流则是随着时间推移零星的提供的一系列的值。
dart:async
包提供一个名叫 StreamController 的对象。StreamController 是实例化 stream 和 sink 的管理器对象。sink 是 stream 的对立面。stream 一直的产生输入,sink 一直的接管输出。
总而言之,BLoCs 是这样一种实体,它们负责解决和存储业务逻辑,应用 sinks 接管输出数据,同时应用 stream 提供数据输入。
地位页面
在应用 app 找到适宜吃饭的中央之前,须要告知 Zomato 你想在哪个地理位置就餐。在本章节,将创立一个简略的页面,蕴含一个头部搜寻区域和一个展现搜寻后果的列表。
Note: 在输出这些代码示例之前,不要遗记关上 DartFmt。它是放弃 Flutter 利用程序代码格调的惟一办法。
在工程的 lib/UI 目录下,创立一个名为 location_screen.dart 的新文件。在文件中增加一个 StatelessWidget
的扩大类,命名为 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'));
}
}
地位页面蕴含一个 TextField
,用户能够在这里输出地理位置信息。
Note: 输出类时,IDE 会提醒谬误,这是因为这些类没有导入。要解决此问题,请将光标移到任何带有红色下划线的符号上,而后,在 macOS 上按 option+enter(在 Windows/Linux 上按 Alt+Enter)或单击红色灯泡。将会弹出一个菜单,在菜单中选择正确的文件进行导入。
创立另外一个文件,main_screen.dart,用来治理 app 的页面流转。增加上面的代码到文件中:
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {return LocationScreen();
}
}
最初,更新 main.dart 以返回新页面。
MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(primarySwatch: Colors.red,),
home: MainScreen(),),
构建并运行 app,看上去应该是这样:
尽管比之前好了一些,但它依然什么都做不了。是时候创立一些 BLoC 了。
第一个 BLoC
在 lib 目录下创立新的目录 BLoC,所有的 BLoC 类将搁置到这里。
在该目录下新建文件 bloc.dart
,并增加如下代码:
abstract class Bloc {void dispose();
}
所有的 BLoC 类都将遵循这个接口。这个接口里只有一个 dispose
办法。须要牢记的一点是,当不再须要流的时候,必须将其敞开,否则会产生内存透露。能够在 dispose
办法中检查和开释资源。
第一个 BLoC 将负责管理 app 的地位抉择性能。
在 BLoC 目录,新建文件 location_bloc.dart, 增加如下代码:
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();
}
}
应用 option+return 导入基类的时候,抉择第二个选项 – Import library package:restaurant_finder/BLoC/bloc.dart。
对所有谬误提醒应用 option+return,直到所有依赖都被正确导入。
LocationBloc
次要实现如下性能:
- 申明了一个 private
StreamController
,治理 BLoC 的 stream 和 sink。StreamController
应用泛型通知类型零碎它将通过 stream 发送何种类型的对象。 - 这行裸露了一个 public 的 getter 办法,调用者通过该办法获取
StreamController
的 stream。 - 该办法是 BLoC 的输出,接管一个
Location
模型对象,将其缓存到公有成员属性_location
,并增加到流的接收器(sink)中。 - 最初,当这个 BLoC 对象被开释时,在清理办法中敞开
StreamController
。否则 IDE 会提醒StreamController
存在内存透露。
到目前为止,第一个 BLoC 曾经实现,接下来创立一个查找地位的 BLoC。
第二个 BLoC
在 BLoC 目录中新建文件 location\_query\_bloc.dart,增加如下代码:
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();
}
}
代码中的 //1
处,是 BLoC 输出端,该办法接管一个字符串类型参数,应用 start 工程中的 ZomatoClient
类从 API 获取地位信息。Dart 的 `async/
await` 语法能够使代码更加简洁。后果返回后将其公布到流(stream)中。
这个 BLoC 与上一个简直雷同,只是这个 BLoC 不仅存储和报告地位,还封装了一个 API 调用。
将 BLoC 注入到 Widget Tree
当初曾经建设了两个 BLoC,须要一种形式将它们注入到 Flutter 的 widget 树。应用 provider
类型的 weidget 已成为 Flutter 的常规。一个 provider 就是一个存储数据的 widget,它可能将数据很好的提供给它所有的子 widget。
通常这是 InheritedWidget
的工作,但因为 BLoC 对象须要被开释,StatefulWidget
将提供雷同的性能。尽管语法有点简单,但后果是一样的。
在 BLoC 目录下新建文件 bloc_provider.dart,并增加如下代码:
// 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();}
}
代码解读如下:
BlocProvider
是一个泛型类,泛型T
被限定为一个实现了BLoC
接口的对象。意味着这个 provider 只能存储 BLoC 对象。of
办法容许 widget tree 的子孙节点应用以后的 build context 检索BlocProvider
。在 Flutter 里这是十分常见的模式。- 这是获取泛型类型援用的通用形式。
build
办法只是返回了 widget 的 child,并没有渲染任何货色。- 最初,这个 provider 继承自
StatefulWidget
的惟一起因是须要拜访dispose
办法。当 widget 从 widget tree 中移除,Flutter 将调用 dispose 办法,该办法将顺次敞开流。
对接地位页面
当初曾经实现了用于查找地位的 BLoC 层,上面将应用该层。
首选,在 main.dart
文件里,在 material app 的下层搁置一个 Location BLoC,用于存储利用状态。最简略的办法是,将光标挪动到 MaterialApp 上方,按下 option+return(Windows/Linux 上是 Alt+Enter),在弹出的菜单中选择 Wrap with a new widget。
Note: 此代码片段的灵感来自 Didier Boelens 的这篇精彩文章 Reactive Programming — Streams — BLoC。这个 widget 没有做任何优化,实践上是能够改良的。出于本文的目标,咱们依然应用这种简略的办法,它在大部分状况下齐全能够承受。如果在 app 生命周期的前期发现它引起了性能问题,能够在 Flutter BLoC Package 中找到更全面的解决方案。
应用 LocationBloc
类型的 BlocProvider
进行包装,并在 bloc
属性地位创立一个 LocationBloc
实例。
return BlocProvider<LocationBloc>(bloc: LocationBloc(),
child: MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(primarySwatch: Colors.red,),
home: MainScreen(),),
);
在 material app 的下层增加 widget,在 widget 里增加数据,这是在多个页面共享拜访数据的好形式。
在主界面 main_screen.dart 中须要做相似的事件。在 LocationScreen
widget 上方点击 option+return,这次抉择 ‘Wrap with StreamBuilder’。更新后的代码如下:
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
是让 BLoC 模式如此美味的秘制酱汁。这些 widget 将主动监听来自 stream 的事件。当一个新的事件达到,builder 闭包函数将被执行来更新 widget tree。应用 StreamBuilder
和 BLoC 模式,在整个教程中都不须要调用 setState() 办法。
在下面的代码中:
- 对于
stream
属性,应用of
办法获取LocationBloc
并将其 stream 增加到StreamBuilder
中。 - 最后 stream 里没有数据,这是齐全失常的。如果没有数据,返回
LocationScreen
。否则,当初仅返回一个空白容器。
下一步,应用之前创立的 LocationQueryBloc
更新 location_screen.dart
中的地位页面。不要遗记应用 IDE 提供的 widget 包装工具更轻松地更新代码。
@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),
)
],
),
),
);
}
在这段代码里:
- 首先,在 build 办法的开始局部实例化了一个新的
LocationQueryBloc
对象。 - 将 BLoC 存储在
BlocProvider
中,BlocProvider 将治理 BLoC 的生命周期。 - 更新
TextField
的onChanged
闭包办法,传递文本到LocationQueryBloc
。这将触发获取数据的调用链,首先调用 Zomato,而后将返回的地位信息发送到 stream 中。 - 将 bloc 传递给
_buildResults
办法。
在 LocationScreen
中增加一个 boolean 字段,用来跟踪这个页面是否是全屏对话框:
class LocationScreen extends StatelessWidget {
final bool isFullScreenDialog;
const LocationScreen({Key key, this.isFullScreenDialog = false})
: super(key: key);
...
这个 boolean 字段仅仅是一个简略标记位(默认值为 false),稍后点击地位信息的时候,用来更新页面导航行为。
当初更新 _buildResults
办法,增加一个 stream builder 并将结果显示在一个列表中。应用 ‘Wrap with StreamBuilder’ 疾速更新代码。
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();}
},
);
},
);
}
在下面的代码中:
- stream 有三个条件分支,返回不同的后果。可能没有数据,意味着用户没有输出任何信息;可能是一个空的列表,意味着 Zomato 找不到任何你想要查找的内容;最初,可能是一个残缺的餐厅列表,意味着每一件事都做的很完满。
- 这里展现地位信息列表。这个办法的行为就是一般的申明式 Flutter 代码。
- 在
onTap
闭包中,应用程序检索位于树根部的LocationBloc
,并通知它用户曾经抉择了一个地位。点击列表项将会导致整个屏幕临时变黑。
持续构建并运行,该应用程序应该从 Zomato 获取地位后果并将它们显示在列表中。
很好!这是真正的提高。
餐厅页面
这个 app 的第二个页面将依据搜寻查问的结果显示餐厅列表。它也有本人的 BLoC 对象,用来治理页面状态。
在 BLoC 目录下新建文件 restaurant_bloc.dart,增加上面的代码:
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();
}
}
代码简直和 LocationQueryBloc
一样,惟一的不同是 API 和返回的数据类型。
在 UI 目录下创立文件 restaurant_screen.dart,以应用新的 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);
},
);
}
}
新建一个独立的 restaurant_tile.dart 文件,用于显示餐厅的详细信息:
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),
);
}
}
代码和地位页面的十分类似,简直是一样的。惟一不同的是这里显示的是餐厅而不是地位信息。
批改 main_screen.dart 文件中的 MainScreen
,当失去地位信息后返回一个餐厅页面。
builder: (context, snapshot) {
final location = snapshot.data;
if (location == null) {return LocationScreen();
}
return RestaurantScreen(location: location);
},
Hot restart 这个 app。选中一个地位,而后搜寻想吃的货色,一个餐厅的列表会呈现在你背后。
看上去很美味。这是谁筹备吃蛋糕了?
珍藏餐厅
到目前为止,BLoC 模式已被用来治理用户输出,但远不止于此。假如用户想要跟踪他们最喜爱的餐厅并将其显示在独自的列表中。这也能够通过 BLoC 模式解决。
在 BLoC 目录下为 BLoC 新建文件 favorite_bloc.dart,用于存储这个列表:
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();
}
}
在 // 1
这里,BLoC 应用一个 Broadcast StreamController
代替惯例的 StreamController
。播送 stream 容许多个监听者,但惯例 stream 只容许一个。后面两个 bloc 不须要播送流,因为只有一个一对一的关系。对于珍藏性能,有两个中央须要同时监听 stream,所以播送在这里是须要的。
Note: 作为通用规定,在设计 BLoC 的时候,应该优先应用惯例 stream,当前面发现须要播送的时候,再将代码批改成应用播送 stream。当多个对象尝试监听同一个惯例 stream 的时候,Flutter 会抛出异样。能够将此看作是须要批改代码的标记。
这个 BLoC 须要从多个页面拜访,意味着须要将其搁置在导航器的上方。更新 main.dart 文件,再增加一个 widget,包裹在 MaterialApp
里面,并且在原来的 provider 外面。
return BlocProvider<LocationBloc>(bloc: LocationBloc(),
child: BlocProvider<FavoriteBloc>(bloc: FavoriteBloc(),
child: MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(primarySwatch: Colors.red,),
home: MainScreen(),),
),
);
接下来在 UI 目录下新建文件 favorite_screen.dart。这个 widget 将用于展现珍藏的餐厅列表:
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);
},
);
},
),
);
}
}
在这个 widget 里:
- 增加初始化数据到
StreamBuilder
。StreamBuilder
将立刻触发对 builder 闭包的执行,即便没有任何数据。这容许 Flutter 确保快照(snapshot)始终有数据,而不是毫无必要的重绘页面。 - 检测 stream 的状态,如果这时还没有建设链接,则应用明确的珍藏餐厅列表代替 stream 中发送的新事件。
更新餐厅页面的 build
办法,增加一个 action,当点击事件触发时将珍藏餐厅页面增加到导航栈中。
@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),
);
}
还须要一个页面,用来将餐厅增加到珍藏餐厅中。
在 UI 目录下新建文件 restaurant\_details\_screen.dart。这个页面大部分是动态的布局代码:
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'),
);
},
);
}
}
在下面代码中:
- 这个 widget 应用珍藏 stream 检测餐厅是否已被珍藏,而后渲染适宜的 widget。
FavoriteBloc
中的toggleRestaurant
办法的实现,使得 UI 不须要关怀餐厅的状态。如果餐厅不在珍藏列表中,它将会被增加进来;反之,如果餐厅在珍藏列表中,它将会被删除。
在 restaurant_tile.dart 文件中增加 onTap
闭包,用来将这个新的页面增加到 app 中。
onTap: () {Navigator.of(context).push(
MaterialPageRoute(builder: (context) =>
RestaurantDetailsScreen(restaurant: restaurant),
),
);
},
构建并运行这个 app。
用户应该能够珍藏、勾销珍藏和查看珍藏列表了。甚至能够从珍藏餐厅页面中删除餐厅,而无需增加额定的代码。这就是流(stream)的力量!
更新地位信息
如果用户想更改他们正在搜寻的地位怎么办?当初的代码实现,如果想更改地位信息,必须重新启动这个 app。
因为曾经将 app 的工作设置为基于一系列的流,所以增加这个性能几乎不费吹灰之力的。甚至就像是在蛋糕上放一颗樱桃一样简略!
在餐厅页面增加一个 floating action button,并将地位页面以模态形式展现:
...
body: _buildSearch(context),
floatingActionButton: FloatingActionButton(child: Icon(Icons.edit_location),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => LocationScreen(
// 1
isFullScreenDialog: true,
),
fullscreenDialog: true)),
),
);
}
在 // 1
处,设置 isFullScreenDialog
的值为 true
。这是咱们之前增加到地位页面的。
之前在为 LocationScreen
编写的 ListTile
中,增加 onTap
闭包时应用过这个标记。
onTap: () {final locationBloc = BlocProvider.of<LocationBloc>(context);
locationBloc.selectLocation(location);
if (isFullScreenDialog) {Navigator.of(context).pop();}
},
这样做的起因是,如果地位页面是以模态形式展示的,须要将它从导航栈中移除。如果没有这个代码,当点击 ListTile
时,什么都不会产生。地位信息 stream 将被更新,但 UI 不会有任何响应。
最初一次构建并运行这个 app。你将看到一个 floating action button,当点击该按钮时,将以模态形式展现地位页面。
而后去哪?
祝贺你把握了 BLoC 模式。BLoC 是一种简略但功能强大的模式,能够帮忙你轻松驯服 app 的状态治理,因为它能够在 widget tree 上高低飞舞。
能够在本教程的 Download Materials 中找到最终的示例我的项目工程,如果想运行最终的示例我的项目,须要先把你的 API key 增加到 zomato_client.dart。
其余值得一看的架构模式有:
- Provider – https://pub.dev/packages/prov…
- Scoped Model – https://pub.dev/packages/scop…
- RxDart – https://pub.dev/packages/rxdart
- Redux – https://pub.dev/packages/redux
同时请查阅 流 (stream) 的官网文档,和对于 BLoC 模式的 Google IO 探讨。
心愿你喜爱本 Flutter BLoC 教程。与平常一样,如果有任何问题或意见,请随时分割我,或者在上面评论!