原文: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,并增加如下代码:
// 1class 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 包装工具更轻松地更新代码。
@overrideWidget 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,当点击事件触发时将珍藏餐厅页面增加到导航栈中。
@overrideWidget 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 教程。与平常一样,如果有任何问题或意见,请随时分割我,或者在上面评论!