原文地址在这里, 作者是 Brian Kayfitz。
这里提一点关于 IDE 的问题,很多人是移动转过来的,所以用 Android Studio 的人很多。其实 Flutter 也可以用 VS Code 来开发。笔者,两个都用过,他们各有好处。Android Studio 在项目初期,目录、文件处理多的时候方便。重构的时候关于文件的修改,都会在其他文件引用里一起修改,删除也会有提示。在 VS Code 里这些没有,改文件名要手动去把 import
也一起改了。但是,VS Code 调试方便很多。但是,在真机调试的时候还是要记得先Select Device。
正文
设计 app 的架构经常会引起争论。每个人都有自己喜欢的一套炫酷的架构和一大堆名词。
iOS 和 Android 开发者都对 MVC 非常了解,并且在开发的时候把这个模式作为默认的架构。Model 和 View 是分开的,Controller 来作为他们沟通的桥梁。
然而,Flutter 带来的一套响应式设计并不能很好的兼容 MVC。一个脱胎于这个经典模式的新的架构就出现在了 Flutter 社区 –BLoC。
BLoC 是Business Logic Components 的缩写。BLoC 的哲学就是 app 里的所有东西都应该被认为是事件流:一部分组件订阅事件,另一部分组件则响应事件。BLoC 居中管理这些会话。Dart 甚至把流(Stream)内置到了语言本身里。
这个模式最好的地方就是你不需要引入任何的插件,也不需要学习其他的语法。所有需要的内容 Flutter 都有提供。
在本文里,我们要新建一个查找餐厅的 app。API 是有 Zomato
提供。最后你会学到以下内容:
- 在 BLoC 模式里包装 API 调用
- 查找并异步显示结果
- 维护一个可以从多个页面访问到的最爱餐厅列表
开始
在这里下载开始项目代码,使用你最喜欢的 IDE 打开。记得开始的时候运行flutter pub get
,在 IDE 里也好,在命令行里也可以。在所有依赖都下载完成后就可以开始编码了。
在开始项目里包含了基本的 model 文件和网络请求文件。看起来是这样的:
获取 API 的 Key
在开始开发应用之前,首先要获得一个我们要用的 API 的 key。在 Zomato 的开发者站点 https://developers.zomato.com/api,注册并生成一个 key。
在 DataLayer
目录下,打开 zomato_client.dart
文件。修改这个常量值:
class ZomatoClient {final _apiKey = "Your api key here";}
实际的开发中把 key 放进源码或者夹杂到版本控制工具里可不是什么明智之举。这里只是为了方便,可不要用在实际的开发里。
运行起来,你会看到这样的效果:
一片黑,现在开始添加代码:
我们来烤一个多层蛋糕
写 app 的时候,不管你用的是 Flutter 或者其他的框架,把类分层都是很关键的。这更像是一个非正式的约定,不是一定要在代码里有怎么样的体现。
每层,或者一组类,都负责一个总体的职责。在初始项目里有一个目录DataLayer。这个数据层专门用来负责 app 的 model 和与后台通信。它对 UI 一无所知。
每个 app 都不尽相同,但是总体来说你都会构建一个这样的东西:
这个架构约定并没有和 MVC 太过不同。UI/Flutter 层只能和 BLoC 层通信,BLoC 层处理逻辑并给数据层和 UI 发送事件。这样的结构可以保证 app 规模变大的时候可以平滑的扩展。
深入 BLoC
BLoC 基本就是基于 Dart 的流(Stream)的。
流,和 Future 一样,也是在 dart:async
包里。一个流就像一个 future,不同的是流不只是异步的返回一个值,流可以随着时间的推移返回很多的值。如果一个 future 最终是一个值的话,那么一个流就是会随着时间可以返回一个系列的值。
dart:async
包提供了一个 StreamController
类。流控制器管理的两个对象流和槽(sink)。sink 和流相对应,流提供提供数据,sink 接受输入值。
总结一下,BLoC 用来处理逻辑,sink 接受输入,流输出。
定位界面
在查找餐馆之前,你要告诉 Zomato 你要在哪里吃饭。在这一节,你要新建一个简单的界面,有一个搜索栏和一个列表显示搜索的结果。
在输入代码之前不要忘记打开DartFmt。这才是编写 Flutter app 的组好编码方式。
在 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
,用户可以在这里输入位置。
你的 IDE 在你输入的类没有被 import 的话会有报错。要改正这个错误的话只要把光标移动到这个标识符上,然后按下苹果系统下option+enter(windows 下 Alt+Enter)或者点一下边上的红色小灯泡。点了之后会出现一个菜单,选择 import 那条就 OK。
添加另一个文件 main_screen.dart 文件,它会用来管理界面的导航。添加如下的代码:
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {return LocationScreen();
}
}
更新 main.dart 文件:
MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(primarySwatch: Colors.red,),
home: MainScreen(),),
现在运行代码,是这样的:
现在到了 BLoC 时间了。
第一个 BLoC
在 lib 目录下创建一个 BLoC 目录。这里用来存放所有的 BLoC 类。
新建一个 bloc.dart 文件,添加如下代码:
abstract class Bloc {void dispose();
}
所有的 BLoC 类都会遵循这个接口。这个接口并没有做什么,只是强制你的代码要包含一个 dispoose
方法。使用流很重要的一点就是不用的时候要关掉,否则会引起内存泄漏。有了 dispose
方法,app 会直接调用。
第一个 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+enter import bloc 类。
LocationBloc
主要处理一下的事情:
- 有一个私有的
StreamController
来管理流和 sink。StreamController
使用泛型来告诉调用代码返回的数据是什么类型的。 - 这一行使用 getter 来暴露流
- 这个方法用来给 BLoC 输入值。并且位置数据也缓存在了
_location
属性里。 - 最终,在清理方法里
StreamController
在这个对象被回收之前被关闭。如果你不这么做,你的 IDE 也会显示出错误。
现在你的第一个 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,这个方法接受一个字符串参数,并且用ZomatoClient
类来获取位置数据。这里用了 async/await
来让代码看起来清晰一些。结果随后会被推进流里。
这个 BLoC 和上一个基本上类似,只是这个里面还包含了一个 API 请求。
把 BLoC 和组件树结合
现在已经有两个 BLoC 了,你需要把他们和组件结合到一起。这样的方式在 Flutter 基本就叫做provider。一个 provider 就是给这个组件和它的子组件提供数据的。
一般来说这是 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
方法允许组件从当前的 context 里获取组件树中的BlocProvider
。这是 Flutter 的常规操作。 - 这里是获取一个泛型类型的对象
- 这个
build
方法并不会构建任何的东西 - 最后,这个 provider 为什么要继承
StatefulWidget
呢,主要是为了dispose
方法。当一个组件从树里移除的时候,Flutter 就会调用dispose
方法关闭流
组合定位界面
你已经有了查找位置的完整的 BLoC 层代码,是时候用起来了。
首先,在 main.dart 里用一个 BLoC 包裹 material app。最简单的就是把光标移动到 MaterialApp
上,按下option+enter(windows 使用 alt+enter),这样会弹出一个菜单,选择Wrap with a new widget。
注意:这段代码是收到 Didier Boelens 的 https://www.didierboelens.com…—streams—bloc/。的启发。这个组件还没有优化,不过理论上是可以优化的。本文会继续使用比较初始的方式,因为这样可以满足大多数的场景。如果之后你发现有性能的问题,那么可以在 Flutter BLoC 包里找到改进的方法。
之后代码就是这样的了:
return BlocProvider<LocationBloc>(bloc: LocationBloc(),
child: MaterialApp(
title: 'Restaurant Finder',
theme: ThemeData(primarySwatch: Colors.red,),
home: MainScreen(),),
);
在 material app 外面包一层 provider 是给需要数据的组件传递数据最简单的方法了。
在 main_screen.dart 文件也要做类似的事情。在 LocationScreen.dart
上按下option + enter,选择 **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 模式的催化剂。这些组件会自动监听流的事件。当收到一个新的事件的时候,builder
方法就会执行,更新组件树。使用 StreamBuilder
和 BLoC 模式就完全不需要 setState
方法了。
代码解析:
-
stream
属性,使用of
方法获取LocationBloc
,并把流交给StreamBuilder
。 - 一开始流是没有数据的,这样很正常。如果没有任何数据 app 就返回
LocationScreen
。否则暂时返回一个空白界面。
接下来,在 location_screen.dart
里面使用 LocationQueryBloc
更新定位界面。不要忘了使用 IDE 提供的快捷键来更新代码:
@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
里面 - 更新
TextField
的onChange
方法,在这里把修改的文字提交给了LocationQueryBloc
对象。这会出发请求 API 并返回数据的链条反应。 - 把 bloc 对象传递给
_buildResult
方法。
给 LocationScreen
添加一个 bool 成员,一次来标记是否是一个全屏对话框。
class LocationScreen extends StatelessWidget {
final bool isFullScreenDialog;
const LocationScreen({Key key, this.isFullScreenDialog = false})
: super(key: key);
...
这个 bool 只是一个简单的标记。以后选中某个位置的时候会用到。
现在更新 _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
并跳转回上一个页面
再次运行代码。你会看到这样的效果:
总算有点进展了。
餐厅页面
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
基类类似。唯一 不同的是返回的数据类型。
现在在 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);
},
你选择了一个定位之后,一列餐厅就可以显示出来了。
最爱餐厅
目前为止,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
的部分,使用了一个 广播(Broadcast)的 StreamController
,而不是一个常规的StreamController
。Broadcast 类型的 stream 可以有多个监听器(listener),而常规的只允许有一个。在前两个 BLoC 里面只存在一对一的关系,所以也不需要多个监听器。对于最喜欢这个功能,需要两个地方去监听,所以广播就是必须的了。
注意:使用 BLoC 的一般规则是使用首先使用常规的流,之后如果需要广播的时候才去重构代码。如果多个对象监听同一个常规的流,那么 Flutter 会抛出一个异常。使用这个来作为需要重构代码的一个标志。
这个 BLoC 需要多个页面都可以访问到,也就是说要放在导航器的外面了。更新main.dart,添加如下的组件:
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 文件。这个组件会显示用户最喜欢的餐厅:
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);
},
);
},
),
);
}
}
在这个组件里:
- 在
StreamBuilder
里添加初始数据。StreamBuilder
会立即调用 builder 方法,即使是没有数据的。 - 检查 app 的连接状态。
接下来更新餐厅界面的 build
方法,把最喜欢的餐厅加到导航里面:
@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'),
);
},
);
}
}
代码解析:
- 这个组件使用了
FavoriteBloc
来判断某个餐厅是否是最喜欢的餐厅,并对应的更新界面 -
FavoriteBloc#toggleRestaurant
方法可以让组件不用去关心某个餐厅是不是最喜欢的。
在 restaurant_tile.dart 文件的 onTap
方法里添加下面的代码:
onTap: () {Navigator.of(context).push(
MaterialPageRoute(builder: (context) =>
RestaurantDetailsScreen(restaurant: restaurant),
),
);
},
运行代码:
更新定位
如果用户想要更新他们查找的定位呢?现在如果你更改了位置,那么 app 就要重启才行。
因为你已经让代码工作在流传递过来的一组数据上了,那么添加一个功能就变得非常的简单,就像在蛋糕上放一个樱桃那么简单。
在餐厅页,添加一个浮动按钮。点下这个按钮之后就会把定位页面弹出来。
...
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
的LisTile#onTap
方法是这么使用 isFullScreenDialog
的:
onTap: () {final locationBloc = BlocProvider.of<LocationBloc>(context);
locationBloc.selectLocation(location);
if (isFullScreenDialog) {Navigator.of(context).pop();}
},
这么做是为了可以在定位也作为对话框显示的时候也可以去掉。
再次运行代码你会看到一个浮动按钮,点了之后就会弹出定位页。
最后
祝贺你已经学会了 BLoC 模式。BLoC 是一个简单而强大的 app 状态管理模式。
你可以在本例里下载到最终的项目代码。如果要运行起来的话,千万记住要先从 zomato 获得一个 app key 并且更新 zomato_client.dart 代码(不要放到代码版本控制里,比如 github 等)。其他可以看的模式:
- 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…
也可以查看官方文档,或者 Google IO 的视频。
希望你喜欢这个 BLoC 教程,有什么问题可以留在评论区里。