关于flutter:Flutter-BLoC-模式入门教程

0次阅读

共计 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 将实现上面的事件:

  1. 应用 BLoC 模式封装 API 调用
  2. 搜寻餐厅并异步显示后果
  3. 保护珍藏列表,并在多个页面展现

筹备开始

下载并应用你最喜爱的 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 次要实现如下性能:

  1. 申明了一个 private StreamController,治理 BLoC 的 stream 和 sink。StreamController 应用泛型通知类型零碎它将通过 stream 发送何种类型的对象。
  2. 这行裸露了一个 public 的 getter 办法,调用者通过该办法获取 StreamController 的 stream。
  3. 该办法是 BLoC 的输出,接管一个 Location 模型对象,将其缓存到公有成员属性 _location,并增加到流的接收器(sink)中。
  4. 最初,当这个 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();}
}

代码解读如下:

  1. BlocProvider 是一个泛型类,泛型 T 被限定为一个实现了 BLoC 接口的对象。意味着这个 provider 只能存储 BLoC 对象。
  2. of 办法容许 widget tree 的子孙节点应用以后的 build context 检索 BlocProvider。在 Flutter 里这是十分常见的模式。
  3. 这是获取泛型类型援用的通用形式。
  4. build 办法只是返回了 widget 的 child,并没有渲染任何货色。
  5. 最初,这个 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() 办法。

在下面的代码中:

  1. 对于 stream 属性,应用 of 办法获取 LocationBloc 并将其 stream 增加到 StreamBuilder 中。
  2. 最后 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),
          )
        ],
      ),
    ),
  );
}

在这段代码里:

  1. 首先,在 build 办法的开始局部实例化了一个新的 LocationQueryBloc 对象。
  2. 将 BLoC 存储在 BlocProvider 中,BlocProvider 将治理 BLoC 的生命周期。
  3. 更新 TextFieldonChanged 闭包办法,传递文本到 LocationQueryBloc。这将触发获取数据的调用链,首先调用 Zomato,而后将返回的地位信息发送到 stream 中。
  4. 将 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();}
        },
      );
    },
  );
}

在下面的代码中:

  1. stream 有三个条件分支,返回不同的后果。可能没有数据,意味着用户没有输出任何信息;可能是一个空的列表,意味着 Zomato 找不到任何你想要查找的内容;最初,可能是一个残缺的餐厅列表,意味着每一件事都做的很完满。
  2. 这里展现地位信息列表。这个办法的行为就是一般的申明式 Flutter 代码。
  3. 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 里:

  1. 增加初始化数据到 StreamBuilderStreamBuilder 将立刻触发对 builder 闭包的执行,即便没有任何数据。这容许 Flutter 确保快照(snapshot)始终有数据,而不是毫无必要的重绘页面。
  2. 检测 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'),
        );
      },
    );
  }
}

在下面代码中:

  1. 这个 widget 应用珍藏 stream 检测餐厅是否已被珍藏,而后渲染适宜的 widget。
  2. 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 教程。与平常一样,如果有任何问题或意见,请随时分割我,或者在上面评论!

正文完
 0