[Flutter学徒] 14 - 流

562 阅读14分钟

本文由 简悦SimpRead 转码,原文地址 www.raywenderlich.com

本章将教你什么是流,如何在你的Flutter应用程序中使用它们,以及它们如何帮助c......。

想象一下,你坐在小河边,享受着美好的时光。在观看水流时,您看到一块木头或一片叶子顺着溪水漂流而下,您决定把它从水中取出。你甚至可以让上游的人特意把东西漂到小河里让你抓。

你可以用类似的方式来想象Dart溪流:作为数据在溪流中流动,等待有人来抓它。这就是流在Dart中的作用--它发送数据事件给监听器来抓取。

使用Dart流,你可以一次发送一个数据事件,而你的应用程序的其他部分则监听这些事件。这些事件可以是集合、地图或任何其他你创建的数据类型。

除了数据之外,流还可以发送错误;如果需要,你还可以停止流。

在本章中,你将更新你的配方项目,在两个不同的地方使用流。你将使用一个书签,让用户标记最喜欢的食谱,并自动更新用户界面以显示它们。你将使用第二个流来更新你的配料和食品清单。

但在你跳入代码之前,你将了解更多关于流的工作原理。

流的类型

流是Dart的一部分,Flutter也继承了它们。在Flutter中,有两种类型的流:单一订阅流和广播流。

单次订阅流是默认的。当你只在一个屏幕上使用一个特定的流时,它们工作得很好。

一个单一的订阅流只能被收听一次。在它有一个监听器之前,它不会开始产生事件,当监听器停止监听时,它就会停止发送事件,即使事件的来源仍然可以提供更多的数据。

单一的订阅流对于下载一个文件或任何单次使用的操作都很有用。例如,一个小部件可以订阅一个流来接收关于一个值的更新,如下载的进度,并相应地更新其用户界面。

如果你需要你的应用程序的多个部分来访问同一个流,请使用一个广播流。

一个广播流允许任何数量的监听者。当它的事件准备好时,无论是否有监听器,它都会触发。

要创建一个广播流,你只需在一个现有的单一订阅流上调用asBroadcastStream()

final broadcastStream = singleStream.asBroadcastStream()。


你可以通过检查其布尔属性isBroadcast来区分广播流和单一订阅流。

在Flutter中,有一些建立在Stream之上的关键类,可以简化流的编程。

下图显示了用于流的主要类。

接下来,你将对每一个进行深入了解。

StreamController和sink

当你创建一个流时,你通常使用StreamController,它同时持有流和StreamSink。下面是一个使用StreamController的例子。

final _recipeStreamController = StreamController<List<Recipe>>();
final _stream = _recipeStreamController.stream;


要向流中添加数据,你要把它添加到它的汇中。

_recipeStreamController.sink.add(_recipesList)。


这使用控制器的sink字段来 "放置 "流中的食谱列表。这些数据将被发送给任何当前的监听器。

当你完成了流的处理,确保你关闭它,像这样。

_recipeStreamController.close()。


StreamSubscription

在一个流上使用listen()会返回一个StreamSubscription。你可以使用这个订阅类在完成后取消流,就像这样。

StreamSubscription s = stream.listen((value) {
    print('Value from controller: $value');
});
...
...
// You are done with the subscription
subscription.cancel();


有时,有一个自动机制来避免手动管理订阅是很有帮助的。这就是StreamBuilder的用武之地。

StreamBuilder

StreamBuilder在你想使用一个流时很方便。它需要两个参数:一个流和一个构建器。当你从流中接收数据时,构建器会负责构建或更新用户界面。

下面是一个例子。

final repository = Provider.of<Repository>(context, listen: false);
  return StreamBuilder<List<Recipe>>(
    stream: repository.recipesStream(),
    builder: (context, AsyncSnapshot<List<Recipe>> snapshot) {
      // extract recipes from snapshot and build the view
    }
  )
...


StreamBuilder很方便,因为你不需要直接使用订阅,而且当widget被销毁时,它会自动从流中取消订阅。

现在你了解了流的工作原理,你将转换你现有的项目来使用它们。

向Recipe Finder添加流

你现在已经准备好开始处理你的食谱项目了。如果你在前面的章节中一直关注着你的应用程序,请打开它并在本章中继续使用它。如果没有,只要找到本章的项目文件夹,在Android Studio中打开starter

注意。如果你使用启动程序,别忘了在network/recipe_service.dart中添加你的apiKeyapiId

为了将你的项目转换为使用流,你需要改变内存库类,增加两个新的方法,为配方和原料分别返回一个流。你将使用流来修改这个列表,并刷新用户界面以显示变化,而不是仅仅返回一个静态食谱的列表。

这就是应用程序的流程,看起来像这样。

在这里,你可以看到,RecipeList屏幕上有一个食谱列表。将一个食谱添加到书签的食谱列表中,并同时更新书签和杂货店屏幕。

你首先要把你的存储库代码转换为返回期货

在版本库中添加期货和流媒体

打开data/repository.dart,将所有的返回类型改为返回Future。例如,将现有的findAllRecipes()改为。

Future<List<Recipe>> findAllRecipes()。


除了 "init() "和 "close()",所有的方法都要这样做。

你的最终类应该是这样的。

Future<List<Recipe>> findAllRecipes();

Future<Recipe> findRecipeById(int id);

Future<List<Ingredient>> findAllIngredients();

Future<List<Ingredient>> findRecipeIngredients(int recipeId);

Future<int> insertRecipe(Recipe recipe);

Future<List<int>> insertIngredients(List<Ingredient> ingredients);

Future<void> deleteRecipe(Recipe recipe);

Future<void> deleteIngredient(Ingredient ingredient);

Future<void> deleteIngredients(List<Ingredient> ingredients);

Future<void> deleteRecipeIngredients(int recipeId);

Future init();

void close();


这些更新允许你拥有异步工作的方法来处理来自数据库或网络的数据。

接下来,在findAllRecipes()后面添加两个新的Stream

// 1
Stream<List<Recipe>> watchAllRecipes();
// 2
Stream<List<Ingredient>> watchAllIngredients();


下面是这段代码的作用。

  1. watchAllRecipes()观察菜谱列表的任何变化。例如,如果用户做了一个新的搜索,它就会更新食谱列表并相应地通知监听器。
  2. 2.watchAllIngredients()监听显示在杂货店屏幕上的成分列表的变化。

你现在已经改变了界面,所以你需要更新内存库。打开data/memory_respository.dart,删除foundation.dart的导入。

清理存储库的代码

在更新代码以使用流和期货之前,有一些小的内务更新。

还是在data/memory_respository.dart,导入Dart **async*库。

import 'dart:async';


然后,更新MemoryRepository类的定义,删除ChangeNotifier,所以它看起来像。

class MemoryRepository extends Repository {


接下来,在现有的两个 "List "声明后添加一些新字段。

//1
Stream<List<Recipe>> _recipeStream;
Stream<List<Ingredient>> _ingredientStream;
// 2
final StreamController _recipeStreamController =
    StreamController<List<Recipe>>();
final StreamController _ingredientStreamController =
    StreamController<List<Ingredient>>();


下面是发生的事情。

  1. _recipeStreamingredientStream是流的私有字段。这些字段将在第一次请求流时被捕获,这可以防止每次调用时都创建新的流。
  2. 为菜谱和配料创建StreamControllers。

现在,在findAllRecipes()前添加这些新方法。

// 3
@override
Stream<List<Recipe>> watchAllRecipes() {
  if (_recipeStream == null) {
    _recipeStream = _recipeStreamController.stream;
  }
  return _recipeStream;
}

// 4
@override
Stream<List<Ingredient>> watchAllIngredients() {
  if (_ingredientStream == null) {
    _ingredientStream = _ingredientStreamController.stream;
  }
  return _ingredientStream;
}


这些流将。

  1. 检查你是否已经有了这个流。如果没有,则调用stream方法,该方法创建一个新的流,然后返回它。
  2. 对成分做同样的事情。

更新现有的资源库

MemoryRepository充满了红色的斜线。这是因为方法都使用了旧的签名,现在所有的东西都是基于Future的。

打开data/memory_repository.dart,像这样更新findAllRecipes()

@override
// 1
Future<List<Recipe>> findAllRecipes() {
  // 2
  return Future.value(_currentRecipes);
}


这些更新。

  1. 改变方法以返回一个 "未来"。
  2. Future.value()来包裹返回值。

在进入下一节之前,你还需要做一些更新。

首先,像这样更新`init()'。

@override
Future init() {
  return Future.value();
}


然后更新close(),使其关闭流。

@override
void close() {
  _recipeStreamController.close();
  _ingredientStreamController.close()。
}


在下一节中,你将更新其余的方法来返回期货,并使用StreamController向流中添加数据。

通过流发送菜谱

正如你前面所学到的,StreamControllersink属性会向流中添加数据。由于这发生在未来,你需要将返回类型改为Future,然后更新方法,将数据添加到流中。

首先,将insertRecipe()改为。

@override
// 1
Future<int> insertRecipe(Recipe recipe) {
  _currentRecipes.add(recipe);
  // 2
  _recipeStreamController.sink.add(_currentRecipes);
  insertIngredients(recipe.ingredients);
  // 3
  // 4
  return Future.value(0);
}


下面是你更新的内容。

  1. 更新方法的返回类型为Future

  2. _currentRecipes添加到配方池中。

  3. 删除了notifyListeners();

  4. 返回一个Future值。你将在后面的章节中学习如何返回新项目的ID。

这就用新的列表替换了以前的列表,并通知任何流式监听器,数据已经改变。

你可能想知道为什么你用同一个列表调用add,而不是添加一个成分或配方。原因是流期望的是一个列表,而不是一个单一的值。通过这种方式,你用更新后的列表替换了之前的列表。

现在你知道如何转换第一个方法,现在是时候转换其余的方法作为练习了。别担心,你可以做到的! :]

练习

转换其余的方法,就像你刚才对insertRecipe()做的那样。你需要做以下工作。

  1. 更新MemoryRepository方法,以返回一个与新的Repository接口方法相匹配的Future
  2. 对于所有改变被监视项目的方法,增加一个调用,将项目添加到水槽中。
  3. 删除所有对`notifyListeners()'的调用。提示:不是所有的方法都有这个语句。
  4. 用`Future'包住返回值。

对于一个返回Future<void>的方法,你认为其返回值会是什么样子?提示:有一个return语句。

return Future.value();


如果你被卡住了,请查看本章文件夹中challenge项目中的memory_repository.dart--但请先尽力试一试吧!

在你完成练习后,MemoryRepository应该不会再有任何红色的方块了--但是在你运行你的新的流驱动的应用程序之前,你还有一些调整工作要做。

服务之间的切换

在上一章中,你创建了一个MockService来提供永不改变的本地数据,但你也可以访问RecipeService。在两者之间切换还是有点繁琐的,所以你要在集成流之前处理好这个问题。

一个简单的方法是使用一个接口--或者,在Dart中被称为抽象类。请记住,一个接口或抽象类只是一个契约,实现类将提供给定的方法。

一旦你创建了你的接口,它将看起来像这样。

要开始创建接口,进入network文件夹,创建一个新的Dart文件,名为service_interface.dart,并添加以下导入。

import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'recipe_model.dart';


接下来,添加一个新的类。

abstract class ServiceInterface {
  Future<Response<Result<APIRecipeQuery>> queryRecipes(
      String query, int from, int to);
}


这定义了一个有一个名为queryRecipes的方法的类。它的参数和返回值与RecipeServiceMockService相同。通过让每个服务实现这个接口,你可以改变提供者来提供这个接口而不是一个特定的类。

实现新的服务接口

打开network/recipe_service.dart,添加service_interface导入。

import 'service_interface.dart';


现在,让RecipeService实现ServiceInterface

abstract class RecipeService extends ChopperService
    implements ServiceInterface {


然后,在@Get(path: 'search')上面添加@override

接下来,在mock_service/mock_service.dart中做同样的事情。添加service_interface的导入。

import '../network/service_interface.dart';


现在,让服务实现这个接口。

class MockService implements ServiceInterface {


并在 "queryRecipes()" 上面添加@override。完成这些后,你现在可以在in.dart中改变提供者。

改变提供者

你现在要采用新的服务接口,而不是你在当前代码中使用的特定服务。

打开main.dart,添加这些导入。

import 'data/repository.dart';
import 'network/recipe_service.dart';
import 'network/service_interface.dart';


然后,删除现有的mock_service.dart的导入。

找到ChangeNotifierProvider<MemoryRepository>并将其改为Provider<Repository>。之后,它应该看起来像。

Provider<Repository>(
  lazy: false,
  create: (_) => MemoryRepository(),
),


当你提供一个Repository时,你可以改变你所创建的仓库的类型。这里,你使用的是MemoryRepository,但你也可以使用其他的东西,正如你在下一章要做的。

现在,将现有的FutureProvider替换为。

Provider<ServiceInterface>(
  create: (_) => RecipeService.create(),
  lazy: false,
),


在这里,你使用RecipeService,但如果你开始遇到API速率限制的问题,你可以切换到MockService

接下来,打开ui/recipes/recipe_details.dart,将memory_repository导入改为Repository导入。

import '././data/repository.dart';


现在,将下面的一行// TODO: change to new repository替换为。

final repository = Provider.of<Repository>(context);


现在,你终于可以用你的接口替换具体的服务了。打开ui/recipes/recipe_list.dart,删除现有的mock_service.dart的导入,添加以下导入。

import '././network/service_interface.dart'

接下来,在_buildRecipeLoader()中,将下面这行// TODO: 替换为新的接口替换为。

future: Provider.of<ServiceInterface>(context).queryRecipes(


现在你已经准备好整合基于流的新代码。系好你的安全带! :]

将流添加到书签中

书签页面使用Consumer,但你想把它改成一个流,这样它就能在用户对食谱进行书签时做出反应。要做到这一点,你需要用Repository替换对MemoryRepository的引用,并使用StreamBuilder小部件。

首先打开ui/myrecipes/my_recipe_list.dart,将memory_repository导入改为。

import '././data/repository.dart';


_buildRecipeList()中,将return Consumer一行和下面的一行替换为以下内容。

// 1
final repository = Provider.of<Repository>(context, listen: false);
// 2
return StreamBuilder<List<Recipe>>(
  // 3
  stream: repository.watchAllRecipes(),
  // 4
  builder: (context, AsyncSnapshot<List<Recipe>> snapshot) {
    // 5
    if (snapshot.connectionState == ConnectionState.active) {
      // 6
      final recipes = snapshot.data ?? [];


暂时不要担心那些红色的方块字。这段代码。

  1. 使用Provider来获取你的Repository
  2. 使用StreamBuilder,它使用List<Recipe>流类型。
  3. 使用新的watchAllRecipes()来返回配方流供创建者使用。
  4. 使用构建器的回调来接收你的快照。
  5. 检查连接的状态。当状态为active时,你就有了数据。

现在,在_buildRecipeList()的末尾,就在最后的});之前,添加。

} else {
  return Container();
}


如果快照还没有准备好,这将返回一个容器。

接下来,在deleteRecipe()中,将MemoryRepository改为Repository,这样看起来像这样。

void deleteRecipe(Repository repository, Recipe recipe) async {


注意。你可以随时使用Android Studio的Code菜单中的Reformat Code命令来清理你的格式化。

所有这些改变都确保了该类依赖于新的、通用的Repository

在这一点上,你已经实现了两个目标中的一个:你已经将食谱屏幕改为使用流。接下来,你将为杂货店标签做同样的事情。

为杂货店添加流

首先,打开ui/shopping/shopping_list.dart,用memory_repository.dart导入替换掉。

import '././data/repository.dart';


就像你在上一节所做的那样,把 "消费者 "条目和它下面的一行改为以下内容。

final repository = Provider.of<Repository>(context);
return StreamBuilder(
  stream: repository.watchAllIngredients(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.active) {
      final ingredients = snapshot.data;
      if (ingredients == null) {
        return Container();
      }


再一次,忽略那些红色的方块。这就像Recipe Details中的代码,只是它使用了watchAllIngredients()

接下来,在ListView.builder的结尾和最后的});之前,添加。

} else {
  return Container();
}


和以前一样,如果快照还没有准备好,这只是返回一个容器。

再也没有红色的方块了。耶! :]

运行你的应用程序,确保它像以前一样工作。你的主屏幕将看起来像这样。

点击一个菜谱。细节页面将看起来像这样。

接下来,点击书签按钮,回到食谱屏幕,然后进入书签页面,查看你刚刚添加的食谱。

最后,进入杂货店标签,确保配方成分全部显示。

恭喜你! 你现在正在使用流来控制数据的流动。如果任何一个屏幕发生变化,其他的屏幕就会知道这个变化,并会更新屏幕。

你还使用了存储库接口,这样你就可以在一个存储类和一个不同的类型之间来回转换。

关键点

  • 流是一种异步发送数据到你的应用程序的其他部分的方式。
  • 你通常通过使用StreamController来创建流。
  • 使用StreamBuilder来添加流到你的用户界面。
  • 抽象类,或接口,是抽象功能的一个好方法。

从哪里开始?

在本章中,你学会了如何使用流。如果你想了解更多关于这个话题的信息,请访问Dart文档dart.dev/tutorials/l…

在下一章中,你将学习数据库以及如何在本地持久化你的数据。