[Flutter学徒] 13 - 状态管理

541 阅读31分钟

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

本章解释了什么是状态管理以及如何用Provider包实现它。你......

UI的主要工作是表示状态。想象一下,例如,你正在从网络上加载一个食谱的列表。当菜谱正在加载时,你显示一个旋转的小部件。当数据加载时,你将旋转器与加载的食谱列表交换。在这种情况下,你从一个加载的状态移动到一个加载的状态。手动处理这种状态变化,而不遵循特定的模式,很快就会导致代码难以理解、更新和维护。一个解决方案是采用一种模式,以编程方式确定如何跟踪变化,以及如何向应用程序的其他部分广播有关状态的细节。这就是所谓的状态管理

为了学习状态管理并看看它是如何工作的,你将继续与之前的项目合作。你也可以通过打开本章的starter项目重新开始。如果你选择这样做,记得点击获取依赖项按钮或从终端执行flutter pub get。你还需要将你的API密钥和ID添加到lib/network/recipe_service.dart

在本章结束时,你会知道。

  • 为什么你需要状态管理。
  • 如何使用Provider实现状态管理。
  • 如何保存当前的书签和原料列表。
  • 如何创建一个资源库。
  • 如何创建一个模拟的服务。
  • 管理状态的不同方法。

架构

当你编写的应用程序的代码随着时间的推移变得越来越大时,你会学会欣赏将代码分离成可管理的部分的重要性。当文件包含一个以上的类或类结合了多种功能时,修复错误和添加新功能就更难了。

处理这个问题的方法之一是遵循清洁架构原则,组织你的项目,使其易于改变和理解。要做到这一点,你要把你的代码分成独立的目录和类,每个类只处理一个任务。你还可以使用接口来定义不同类可以实现的契约,使你可以轻松地交换不同的类或在其他应用程序中重复使用类。

你应该用下面的一些或全部组件来设计你的应用程序。

注意,用户界面与业务逻辑是分开的。启动一个应用程序并把你的数据库和业务逻辑放到你的UI代码中是很容易的--但是当你需要改变你的应用程序的行为时,而这个行为又分布在你的UI代码中,会发生什么呢?这使得它很难改变,并导致重复的代码,你可能会忘记更新。

这些层之间的沟通也很重要。一个层如何与另一个层对话?最简单的方法是在你需要的时候创建这些类。但这将导致同一个类的多个实例,从而导致协调调用的问题。

例如,如果两个类都有自己的数据库处理类,并且对数据库进行了冲突的调用,怎么办?Android和iOS都使用依赖注入DI来在一个地方创建实例,并将它们注入到需要它们的其他类中。本章将介绍Provider包,它做了类似的事情。

最终,业务逻辑层应该负责决定如何对用户的行为做出反应,以及如何将检索和保存数据等任务委托给其他类。

为什么你需要状态管理

首先,术语状态状态管理是什么意思?状态是指一个部件处于活动状态并在内存中存储其数据。Flutter框架处理一些状态,但正如前面提到的,Flutter是声明性的。这意味着当状态或数据发生变化时,或者当你的应用程序的另一部分使用它时,它会从内存中重建一个UIStatefulWidget

状态管理,顾名思义,就是你如何管理你的小部件和应用程序的状态。

要考虑的两种状态类型是短暂的状态,也被称为UI状态应用状态

  • 当widget树中没有其他组件需要访问widget的数据时,使用Ephemeral state。例如,是否选择了TabBarView标签或FloatingActionButton被按下。
  • 当你的应用程序的其他部分需要访问widget的状态数据时,使用App state。一个例子是一个随时间变化的图像,比如当前天气的图标。另一个例子是用户在一个屏幕上选择的信息,然后应该在另一个屏幕上显示,比如当用户在购物车中添加一个项目时。

接下来,你将了解更多关于不同类型的状态以及它们如何适用于你的食谱应用程序。

小工具状态

在第四章 "理解小部件"中,你看到了无状态和有状态小部件之间的区别。一个无状态的widget是以它被创建时的状态绘制的。一个有状态的widget保留了它的状态,并在将来使用它来(重新)绘制自己。

你当前的食谱屏幕有一张卡片,上面有先前的搜索列表,还有一个GridView,上面有一个食谱列表。

左边显示了一些RecipeList小部件,右边显示了存储每个小部件使用的信息的状态对象。一个元素树同时存储了widget本身和RecipeList中所有有状态的widget的状态。

如果一个widget的状态更新了,状态对象也会更新,widget会以更新的状态重新绘制。

这种管理方式只处理特定widget的状态。但是如果你想管理整个应用程序的状态或者在部件和屏幕之间共享状态呢?你可以使用应用状态来实现。

应用状态

在Flutter中,一个有状态的widget可以持有状态,它的孩子可以访问这些状态,并在其构造函数中将数据传递给另一个屏幕。然而,这使你的代码变得复杂,你必须记住将数据对象从树上传下来。如果子部件可以很容易地访问它们的父部件的数据,而不需要传入这些数据,那不是很好吗?

有几种不同的方法来实现这一点,既有内置的部件,也有第三方的包。你会先看一下内置的小部件。

在你的应用程序中管理状态

你的应用程序需要保存三样东西:显示在食谱屏幕上的列表、用户的书签和原料。在本章中,你将使用状态管理来保存这些信息,以便其他屏幕可以使用它。

在这一点上,你将只在内存中保存这些数据,所以当用户重新启动应用程序时,这些选择将不可用。第15章,"用SQLite保存数据",将展示如何将这些数据保存在本地的数据库中,以便更持久地保存。

这些方法仍然适用于屏幕之间的数据共享。下面是一个关于你的类将会是什么样子的大致概念。

有状态的部件

StatefulWidget是保存状态的最基本方法之一。例如,RecipeList小组件为以后的使用保存了几个字段,包括当前的搜索列表和用于分页的搜索结果的开始和结束位置。

当你创建一个有状态的widget时,你会调用createState(),它在Flutter内部存储状态,以便在父代需要重建widget树时重新使用。当widget被重建时,Flutter会重新使用现有的状态。

你使用initstate()进行一次性的工作,比如初始化文本控制器。然后你用setState()来改变状态,触发用新的状态来重建widget。

例如,在第9章 "共享偏好 "中,你用setState()来设置所选标签。这告诉系统要重建UI来选择一个页面。StatefulWidget对于维护内部状态是很好的,但对于widget之外的状态却不是。

实现允许在widget之间共享状态的架构的一种方法是采用InheritedWidget

继承Widget

InheritedWidget是一个内置的类,允许其子部件访问其数据。它是很多其他状态管理部件的基础。如果你创建了一个扩展了InheritedWidget的类,并给它一些数据,任何子部件都可以通过调用context.dependOnInheritedWidgetOfExactType<class>()访问这些数据。

哇,这可真够绕口的! 如下所示,<class>代表扩展InheritedWidget的类的名称。

class RecipeWidget extends InheritedWidget {
  final Recipe recipe;
  RecipeWidget({this.recipe, Widget child}) : super(child: child);

  @override
  bool updateShouldNotify(RecipeWidget oldWidget) => recipe != oldWidget.recipe;

  static RecipeWidget of(BuildContext context) => context.inheritFromWidgetOfExactType(RecipeWidget);
}


然后你就可以从该小部件中提取数据。由于调用的方法名称太长,惯例是创建一个of()方法。

注意updateShouldNotify()比较两个菜谱,这需要Recipe实现equals。否则,你需要比较每个字段。

那么一个子部件,比如显示菜谱标题的文本字段,就可以直接使用。

RecipeWidget recipeWidget = RecipeWidget.of(context);
print(recipeWidget.recipe.label);


使用InheritedWidget的好处是它是一个内置的widget,所以你不需要担心使用外部包。

使用InheritedWidget的缺点是,除非你重建整个widget树,否则配方的值不会改变,因为InheritedWidget是不可改变的。所以,如果你想改变显示的食谱标题,你就必须重建整个RecipeWidget

有一段时间,scoped_modelpub.dev/packages/sc…)是一个有趣的解决方案。它来自Fuchsia代码库,建立在InheritedWidget之上,将UI和数据分开,使这个过程比直接使用InheritedWidget更容易。

然而,自2018年11月发布1.0.0版本以来,谷歌开始推荐Provider作为一个更好的解决方案,提供与scoped_model类似的功能和更多的功能。你将使用Provider来实现你的应用程序中的状态管理。

Provider

Remi Rousselet设计了Provider来包裹InheritedWidget,简化了它。Google已经创建了他们自己的包来处理状态管理,但发现Provider更好。他们现在推荐使用它。

Google甚至在他们的状态管理文档flutter.dev/docs/develo…中详细介绍了它。

从本质上讲,Provider是一套简化了在InheritedWidget之上构建状态管理解决方案的类。

Provider使用的类

Provider有几个常用的类,你会详细了解。ChangeNotifierProviderConsumerFutureProviderMultiProviderStreamProvider

Flutter SDK中构建的关键类之一是ChangeNotifier

ChangeNotifier

ChangeNotifier是一个添加和删除监听器的类,然后将任何变化通知这些监听器。你通常为模型扩展这个类,这样你就可以在模型变化时发送通知。当模型中的某些东西发生变化时,你可以调用notifyListeners(),不管是谁在监听,都可以使用新变化的模型来重新绘制一块用户界面,例如。

ChangeNotifierProvider

ChangeNotifierProvider是一个小部件,它包装了一个类,实现了ChangeNotifier和另一个小部件。当变化被广播时,该widget重建它的树。语法看起来像这样。

ChangeNotifierProvider(
    create: (context) => MyModel(),
    child: <widget>,
);


Provider提供了一个管理状态的中心点,这样你就不需要在每次状态改变时通过setState()手动更新不同的widget。

但是如果你每次都在那个widget中创建一个模型,会发生什么?这将创建一个新的模型! 那个模型所做的任何工作在下次都会丢失。通过使用create,Provider保存了那个模型,而不是每次都重新创建它。

消费者

Consumer是一个小部件,它监听实现ChangeNotifier的类的变化,然后在发现任何变化时重建自己下面的小部件。当建立你的widget树时,尽量把Consumer放在UI层次结构的最深处,这样更新就不会重新创建整个widget树。

Consumer<MyModel>(
  builder: (context, model, child) {
    return Text('Hello ${model.value}');
  }
);


如果你只需要访问模型,而不需要在数据变化时发出通知,可以使用Provider.of,像这样。

Provider.of<MyModel>(context, listen: false).<method name>


listen: false表示你不希望收到任何更新的通知。在initState()中使用Provider.of()时需要这个参数。

FutureProvider

FutureProvider和其他提供者一样工作,使用所需的create参数,返回一个Future

FutureProvider(
  create: (context) => createFuture(),
  child: <widget>,
);

Future<MyModel> createFuture() async {
  return Future.value(MyModel());
}


当一个值不是现成的,但在未来会有的时候,Future是很方便的。例子包括从互联网上请求数据或异步从数据库中读取数据的调用。

MultiProvider

如果你需要一个以上的提供者怎么办?你可以对它们进行嵌套,但会变得很混乱,使它们难以阅读和维护。

Provider<MyModel>(
  create: (_) => Something(),
  child: Provider<MyDatabase>(
   create: (_) => SomethingMore()
       child: <widget>
    ),
);


相反,使用MultiProvider来创建一个提供者列表和一个单一的

MultiProvider(
        providers: [
          Provider<MyModel>(create: (_) => Something()),
          Provider<MyDatabase>(create: (_) => SomethingMore()),
        ],
child: <widget>
);


StreamProvider

你将在下一章中详细了解流的情况。现在,你只需要知道Provider也有一个专门针对流的提供者,其工作方式与FutureProvider相同。当数据通过流进来并且数值随时间变化时,流提供者很方便,例如,当你监控一个设备的连接性时。

现在是时候使用Provider来管理**Recipe Finder的状态了。下一步是把它添加到项目中。

使用Provider

打开pubspec.yaml,在logging后面添加以下包。

provider: ^4.3.3
equatable: ^1.2.6


提供者包含上面提到的所有类。Equatable通过提供equals()toString()以及hashcode帮助进行平等检查。这允许你在地图中检查模型的平等性,这对提供者来说是必要的。

运行Pub Get来安装新的包。

UI模型

在前面的章节中,你为食谱的API创建了模型。在这里,你将创建简单的模型来在屏幕之间共享数据。

lib,创建一个名为data的新目录,并在其中创建一个名为repository.dart的新文件。现在让该文件为空。

data中,创建一个名为models的新目录。在其中创建一个名为ingredient.dart的新文件,并添加以下类。

import 'package:equatable/equatable.dart';
// 1
class Ingredient extends Equatable {
  // 2
  int id;
  int recipeId;
  final String name;
  final double weight;

  // 3
  Ingredient({this.id, this.recipeId, this.name, this.weight});

  // 4
  @override
  List<Object> get props => [recipeId, name, weight];
}


下面是这段代码中发生的事情。

  1. Ingredient类扩展了Equatable,以提供对平等检查的支持。
  2. 添加一个成分所需的属性。你没有把recipeIdid声明为final,这样你就可以在以后改变它。
  3. 声明一个带有所有字段的构造函数。
  4. 当进行平等检查时,Equatable使用props值。在这里,你提供你想用来检查平等性的字段。

下一步是创建一个类来模拟一个食谱。

创建菜谱类

models中,创建recipe.dart然后添加以下代码。

import 'package:equatable/equatable.dart';
import 'ingredient.dart';

class Recipe extends Equatable {
  // 1
  int id;
  final String label;
  final String image;
  final String url;
  // 2
  List<Ingredient> ingredients;
  final double calories;
  final double totalWeight;
  final double totalTime;

  // 3
  Recipe(
      {this.id,
      this.label,
      this.image,
      this.url,
      this.calories,
      this.totalWeight,
      this.totalTime});

  // 4
  @override
  List<Object> get props =>
      [label, image, url, calories, totalWeight, totalTime];
}


该代码包括

  1. Recipe属性,用于配方文本。label, imageurlid不是最终的,所以你可以更新它。
  2. 食谱包含的成分列表,以及其卡路里、重量和烹饪时间。
  3. 一个包含所有字段的构造函数,除了成分,你将在后面添加。
  4. 可等价的属性,你将使用这些属性进行比较。

与其多次导入相同的文件,你将使用一个单一的Dart文件来导出所有你需要的文件。从本质上讲,你将把多个导入文件组合成一个单一的文件。

models中,创建models.dart并添加以下内容来导出你的两个模型文件。

export 'recipe.dart';
export 'ingredient.dart';


现在,你可以直接导入models.dart,而不是每次需要时都要同时导入recipe.dartingredient.dart

随着模型的到位,现在是时候实现从通过网络接收的数据到模型对象的转换。

将数据转换为模型来显示

打开lib/network/recipe_model.dart,导入你新的models.dart文件。

import '../data/models/models.dart';


然后,到文件的底部,在 "APIIngredients "下面,添加一个新方法,将网络原料模型转换为显示原料模型。

List<Ingredient> convertIngredients(List<APIIngredients> apiIngredients) {
  // 1
 final ingredients = <Ingredient>[];
  // 2
  apiIngredients.forEach((ingredient) {
    ingredients
        .add(Ingredient(name: ingredient.name, weight: ingredient.weight));
  });
  return ingredients;
}


在这段代码中,你。

  1. 创建一个新的成分列表来返回。
  2. 将每个APIIngredient转换为Ingredient的实例,并将其添加到列表中。

现在你已经准备好创建一个资源库来处理食谱的创建、获取和删除。

创建一个资源库

接下来,你将创建一个存储库接口来提供、添加和删除配方和配料。

创建data/repository.dart并添加以下内容。

import 'models/models.dart';

abstract class Repository {
  // TODO: Add find methods

  // TODO: Add insert methods

  // TODO: Add delete methods

  // TODO: Add initializing and closing methods
}


记住,Dart没有接口这个关键词;相反,它使用抽象类。这意味着你需要添加你希望所有资源库都实现的方法。接下来你会这样做。

寻找菜谱和配料

// TODO: 添加查找方法替换为以下内容,以帮助查找食谱和配料。

// 1
List<Recipe> findAllRecipes();

// 2
Recipe findRecipeById(int id);

// 3
List<Ingredient> findAllIngredients();

// 4
List<Ingredient> findRecipeIngredients(int recipeId);


在这段代码中,你定义了以下接口。

  1. 返回存储库中的所有配方。
  2. 通过它的ID找到一个特定的配方。
  3. 返回所有成分。
  4. 找到给定食谱ID的所有原料。

添加配方和原料

接下来,替换// TODO: 增加插入方法来插入一个新的食谱和任何原料。

// 5
int insertRecipe(Recipe recipe);

// 6
List<int> insertIngredients(List<Ingredient> ingredients);


在这里,你声明方法来。

  1. 插入一个新的配方。
  2. 添加所有给定的原料。

删除不需要的配方和原料

然后,将// TODO: 增加删除方法替换为包括删除方法。

// 7
void deleteRecipe(Recipe recipe);

// 8
void deleteIngredient(Ingredient ingredient);

// 9
void deleteIngredients(List<Ingredient> ingredients);

// 10
void deleteRecipeIngredients(int recipeId);


在这段代码中,你添加了方法来。

  1. 删除给定的配方。
  2. 删除给定的成分。
  3. 删除所有给定的成分。
  4. 删除给定食谱ID的所有原料。

初始化和关闭资源库

现在,你要添加两个最后的方法。将// TODO: 添加初始化和关闭方法替换为:

// 11
Future init();
// 12
void close();


在这最后一点代码中,你。

  1. 允许资源库初始化。数据库可能需要做一些启动工作。
  2. 关闭存储库。

现在你已经定义了接口,你需要创建一个具体的实现,在内存中存储这些项目。

创建一个内存存储库

存储库是你在内存中存储成分的地方。这是一个临时的解决方案,因为每次你重新启动应用程序时,它们都会丢失。

data中,创建一个名为memory_repository.dart的新文件并添加这些导入。

import 'dart:core';
import 'package:flutter/foundation.dart';
// 1
import 'repository.dart';
// 2
import 'models/models.dart';


要把这段代码分解开来。

  1. repository.dart包含接口定义。
  2. models.dart导出RecipeIngredient类定义。

定义内存库

现在,通过添加以下内容来定义MemoryRepository

注意: MemoryRepository会有红色的斜线,直到你完成添加所有需要的方法。

// 3
class MemoryRepository extends Repository with ChangeNotifier {
  // 4
  @override
  Future init() {
    return Future.value(null);
  }

  @override
  void close() {}

  // 5
  final List<Recipe> _currentRecipes = <Recipe>[];
  // 6
  final List<Ingredient> _currentIngredients = <Ingredient>[];

  // TODO: Add find methods

  // TODO: Add insert methods

  // TODO: Add delete methods

}


下面是这段代码中的内容。

  1. MemoryRepository扩展了Repository并使用Flutter的ChangeNotifier来启用监听器,并将任何变化通知这些监听器。
  2. 由于这是一个内存存储库,你需要方法来初始化和关闭它。
  3. 你初始化你当前的配方列表。
  4. 然后你初始化你当前的成分列表。

因为这个类只是保存到内存中,所以你用列表来存储菜谱和原料。

现在,你已经准备好添加查找、插入和删除食谱数据所需的方法了。

寻找存储的食谱和原料

// TODO: 添加查找方法替换为这些。

@override
List<Recipe> findAllRecipes() {
  // 7
  return _currentRecipes;
}

@override
Recipe findRecipeById(int id) {
  // 8
  return _currentRecipes.firstWhere((recipe) => recipe.id == id);
}

@override
List<Ingredient> findAllIngredients() {
  // 9
  return _currentIngredients;
}

@override
List<Ingredient> findRecipeIngredients(int recipeId) {
  // 10
  final recipe =
      _currentRecipes.firstWhere((recipe) => recipe.id == recipeId);
  // 11
  final recipeIngredients = _currentIngredients
      .where((ingredient) => ingredient.recipeId == recipe.id)
      .toList();
  return recipeIngredients;
}


这段代码。

  1. 返回您当前的RecipeList

  2. 使用firstWhere来寻找具有给定ID的配方。

  3. 返回您当前的原料清单。

  4. 找到一个具有给定ID的配方。

  5. 使用where来寻找所有具有给定食谱ID的原料。

这些方法有助于找到你需要在屏幕上显示的任何食谱或原料集。

添加食谱和配料表

用这些方法代替// TODO: 添加插入方法,让你添加菜谱和配料表。

@override
int insertRecipe(Recipe recipe) {
  // 12
  _currentRecipes.add(recipe);
  // 13
  insertIngredients(recipe.ingredients);
  // 14
  notifyListeners();
  // 15
  return 0;
}

@override
List<int> insertIngredients(List<Ingredient> ingredients) {
  // 16
  if (ingredients != null && ingredients.length != 0) {
    // 17
    _currentIngredients.addAll(ingredients);
    // 18
    notifyListeners();
  }
  // 19
  return <int>[];
}


在这段代码中,你。

  1. 将配方添加到你的列表中。

  2. 调用方法来添加配方的所有成分。

  3. 通知所有听众这些变化。

  4. 返回新菜谱的ID。因为你不需要它,所以它将永远返回0。

  5. 检查以确保有一些成分。

  6. 将所有的原料添加到你的列表中。

  7. 通知所有听众有关变化。

  8. 返回添加的ID的列表。暂时是一个空的列表。

删除配方和原料

// TODO: 添加删除方法替换为这些,以删除配方或原料。

@override
void deleteRecipe(Recipe recipe) {
  // 20
  _currentRecipes.remove(recipe);
  // 21
  deleteRecipeIngredients(recipe.id);
  // 22
  notifyListeners();
}

@override
void deleteIngredient(Ingredient ingredient) {
  // 23
  _currentIngredients.remove(ingredient);
}

@override
void deleteIngredients(List<Ingredient> ingredients) {
  // 24
  _currentIngredients
      .removeWhere((ingredient) => ingredients.contains(ingredient));
  notifyListeners();
}

@override
void deleteRecipeIngredients(int recipeId) {
  // 25
  _currentIngredients
    .removeWhere((ingredient) => ingredient.recipeId == recipeId);
  notifyListeners();
}


在这里,你

  1. 从你的列表中删除配方。
  2. 删除这个食谱的所有原料。
  3. 通知所有听众,数据已经改变。
  4. 从列表中删除原料。
  5. 删除传入列表中的所有成分。
  6. 遍历所有原料,寻找具有给定食谱ID的原料,然后删除它们。

现在你有一个完整的记忆库类,可以查找、添加和删除配方和原料。你将在整个应用程序中使用这个存储库。

通过提供者使用存储库

现在是时候使用你新创建的存储库和Provider了。打开in.dart,添加这些导入。

import 'package:provider/provider.dart';
import 'data/memory_repository.dart';


现在,将MyApp中的build()方法替换为。

Widget build(BuildContext context) {
  // 1
  return ChangeNotifierProvider<MemoryRepository>(
    // 2
    lazy: false,
    // 3
    create: (_) => MemoryRepository(),
    // 4
    child: MaterialApp(
      title: 'Recipes',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.light,
        primaryColor: Colors.white,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MainScreen(),
    ));
}


在这段代码中,你。

  1. 使用ChangeNotifierProvider,其类型为MemoryRepository
  2. lazy设置为false,这样就可以立即创建版本库,而不是等到你需要它的时候。当版本库需要做一些后台工作来启动时,这很有用。
  3. 创建你的版本库。
  4. 返回MaterialApp作为子部件。

注意。如果你的代码在保存修改时没有自动格式化,请记住,你可以随时通过进入代码菜单并选择重新格式化代码来重新格式化它。

模型的代码已经全部到位了。现在是在用户界面中使用它的时候了。

使用存储库的菜谱

你将实现代码,在书签屏幕上添加一个菜谱,在杂货屏幕上添加配料。打开ui/recipes/recipe_details.dart,添加以下导入。

import 'package:provider/provider.dart';
import '../../network/recipe_model.dart';
import '../../data/models/models.dart';
import '../../data/memory_repository.dart';


这包括Provider包、模型和资源库。要显示一个配方的细节,你需要传入你想显示的配方。

替换下面这行 // TODO: 替换为新的构造函数

final Recipe recipe;
const RecipeDetails({Key key, this.recipe}) : super(key: key);


显示菜谱的细节

你需要在细节页上显示配方的图片、标签和卡路里。存储库已经存储了你当前所有的书签菜谱。

还是在ui/recipes/recipe_details.dart中,把这个作为build()方法的第一行。

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


这个方法使用Provider来检索在in.dart中创建的资源库。你将使用它来添加书签。

注意:如果你的recipe_details.dart文件没有//TODO注释,请看一下启动项目的后续几步。

找到第一个//TODO 1注释,将硬编码的imageUrl替换为。

imageUrl: recipe.image,


// TODO 2处,将鸡肉维苏威的文字替换为。

recipe.label,


// TODO 3下面的一行替换为:

label: Text(getCalories(recipe.calories)),


getCalories()来自recipe_model.dart

接下来,你将使用户能够点击书签按钮,将该食谱添加到书签列表中。

将食谱加入书签

第一步是将菜谱插入到资源库中。

ui/recipes/recipe_details.dart中,在//TODO 4之后,添加。

repository.insertRecipe(recipe);


这样就把配方添加到你的仓库中。要接收该配方,你需要更新recipe_list.dart,将该配方发送到详情页。

打开ui/recipes/recipe_list.dart,添加模型导入。

import '../../data/models/models.dart';


然后到_buildRecipeCard(),将return const RecipeDetails();改为。

final detailRecipe = Recipe(
    label: recipe.label,
    image: recipe.image,
    url: recipe.url,
    calories: recipe.calories,
    totalTime: recipe.totalTime,
    totalWeight: recipe.totalWeight);

detailRecipe.ingredients = convertIngredients(recipe.ingredients);
return RecipeDetails(recipe: detailRecipe);


这就从网络配方中创建了一个新的Recipe

现在,建立并运行该应用程序。在搜索框中输入,然后点击放大镜来执行搜索。你会看到类似这样的东西。

选择一个菜谱,进入详细信息页面。

点击书签按钮,详情页将消失。现在,选择书签标签。在这一点上,你会看到一个空白的屏幕--你还没有实现它。接下来你将添加该功能。

实现书签屏幕

ui/myrecipes中,打开my_recipes_list.dart并添加以下导入。

import 'package:provider/provider.dart';
import '../../data/models/recipe.dart';
import '../../data/memory_repository.dart';


这包括检索存储库的提供者以及Recipe类。

在书签页面,用户可以通过向左或向右滑动并选择删除图标来删除书签的食谱。为了实现这一点,在_MyRecipesListState类的底部添加deleteRecipe()

void deleteRecipe(MemoryRepository repository, Recipe recipe) async {
  // 1
  repository.deleteRecipeIngredients(recipe.id);
  // 2
  repository.deleteRecipe(recipe);
  // 3
  setState(() {});
}


在这段代码中,你使用了

  1. 存储库来删除任何配方成分。
  2. 存储库来删除配方。
  3. setState()来重新绘制视图。

还是在_MyRecipesListState中,将下面的一行//TODO 1替换为。

List<Recipe> recipes;


// TODO 2中,删除initState()的整个定义,因为你将从存储库中填充列表。

// TODO 3改为以下内容。

return Consumer<MemoryRepository>(builder: (context, repository, child) {
  recipes = repository.findAllRecipes() ?? [];


这就创建了Consumer,它接收存储库。记住,Consumer是一个可以从父级Provider接收类的部件。

方法findAllRecipes()将返回所有当前的食谱或一个空列表。

// TODO 4处,添加。

final recipe = recipes[index];


ListView.builder返回当前索引,检索该索引的配方。

现在,替换所有硬编码的值。

// TODO 5处,用imageUrl替换。

imageUrl: recipe.image,


// TODO 6处,将title替换为:

title: Text(recipe.label),


// TODO 7中,将onTap()的声明替换为:

onTap: () => deleteRecipe(
    repository,
    recipe)),


这将调用deleteRecipe(),你在上面定义了它。

// TODO 8处,将另一个onTap()替换为。

onTap: () => deleteRecipe(
	  repository,
    recipe)),


最后,将// TODO 9替换为。

  },
);


热重新加载应用程序,并确保你先前收藏的配方现在显示出来。你会看到。

你就快完成了,但杂货店视图目前是空白的。你的下一步是添加显示书签菜谱成分的功能。

实现杂货店屏幕

打开ui/shopping/shopping_list.dart,添加以下内容。

import 'package:provider/provider.dart';
import '../../data/memory_repository.dart';


这里你导入了Provider和存储库。

删除TODO 1下面的一行。

//TODO 2替换为:

return Consumer<MemoryRepository>(builder: (context, repository, child) {
  final ingredients = repository.findAllIngredients() ?? [];


这增加了一个Consumer部件来显示当前的成分。

把下面这行// TODO 3替换为:

title: Text(ingredients[index].name),


这将显示原料的名称属性。

// TODO 4处,添加。

  },
);


这样就关闭了Consumer小部件。

热重新加载,确保你仍然有一个书签保存。

现在,进入杂货店标签,查看你书签上的食谱的成分。你会看到像这样的东西。

恭喜你,你成功了! 由于Provider的基础设施,你现在有了一个可以在不同屏幕上监测和通知状态变化的应用程序。

但还有一件事需要考虑。每次你想尝试一个代码的变化时,用一个请求来打击真正的网络服务器并不是一个好主意。它很耗时,而且不能让你控制返回的数据。你的下一步是建立一个模拟服务,返回模仿真实API的特定响应。

使用模拟服务

你将添加一个替代的方法来检索数据。这很方便,因为。

  • Edamam网站限制了你对开发者账户的查询数量。
  • 拥有一个返回模拟数据版本的服务是很好的做法,特别是为了测试。

assets中,有两个配方JSON文件。你将创建一个模拟服务提供者,随机返回这些文件中的一个。

如果你从生产服务器检索数据时遇到错误,你可以直接用模拟服务交换当前的存储库。

首先,在lib下创建一个名为mock_service的新目录。接下来,在这个新目录下创建mock_service.dart

添加导入的内容。

import 'dart:convert';
import 'dart:math';

// 1
import 'package:chopper/chopper.dart';
// 2
import 'package:flutter/services.dart' show rootBundle;
import '../network/model_response.dart';
import '../network/recipe_model.dart';


下面是你要做的事情。

  1. 你导入Chopper来创建Response的实例。
  2. "显示 "意味着你希望一个或多个特定的类在你的应用程序中是可见的。在这个例子中,你希望rootBundle在加载JSON文件时是可见的。

注意:你可以通过使用hide来隐藏类。

现在,添加MockService

class MockService {
  // 1
  APIRecipeQuery _currentRecipes1;
  APIRecipeQuery _currentRecipes2;
  // 2
  Random nextRecipe = Random();

  // TODO 1: Add create and load methods

  // TODO 2: Add query method

}


下面是这段代码的作用。

  1. 使用_currentRecipes1_currentRecipes2来存储从两个JSON文件加载的结果。
  2. nextRecipeRandom的一个实例,它创建了一个0到1之间的数字。

接下来,你将从JSON文件中加载菜谱。

实现创建和加载配方的方法

现在,将TODO 1替换为。

// 3
void create() {
  loadRecipes();
}

void loadRecipes() async {
  // 4
  var jsonString = await rootBundle.loadString('assets/recipes1.json');
  // 5
  _currentRecipes1 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
  jsonString = await rootBundle.loadString('assets/recipes2.json');
  _currentRecipes2 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
}



要分解这段代码。

  1. create()方法,Provider将调用,只是调用loadRecipes()
  2. rootBundle将JSON文件加载为一个字符串。
  3. jsonDecode()创建一个地图,APIRecipeQuery将使用该地图来获得一个食谱列表。

接下来,将TODO 2替换为以下内容。

Future<Response<Result<APIRecipeQuery>>> queryRecipes(
  String query, int from, int to) {
    // 6
    switch(nextRecipe.nextInt(2)) {
      case 0:
        // 7
        return Future.value(
            Response(null, Success<APIRecipeQuery>(_currentRecipes1)));
      case 1:
        return Future.value(
            Response(null, Success<APIRecipeQuery>(_currentRecipes2)));
      default:
        return Future.value(
            Response(null, Success<APIRecipeQuery>(_currentRecipes1)));
    }
  }


在这里,你

  1. 使用你的随机字段挑选一个随机的整数,要么是0,要么是1。
  2. 将你的 "APIRecipeQuery "结果包裹在 "Success"、"Response"和 "Future"中。

你会注意到这看起来像RecipeService的方法。这是因为模拟的服务应该看起来是一样的。

这就是模拟的全部内容。现在你将在应用程序中使用它,而不是使用真正的服务。

使用模拟服务

添加MockServicein.dart

import 'mock_service/mock_service.dart';


目前,build()正在使用ChangeNotifierProvider。现在,你需要使用多个提供者,所以它也可以使用MockService。`MultiProvider'将完成这个任务。

把整个build()方法替换成这样。

Widget build(BuildContext context) {
  return MultiProvider(
    // 1
    providers: [
      // 2
      ChangeNotifierProvider<MemoryRepository>(
        lazy: false,
        create: (_) => MemoryRepository(),
      ),
      // 3
      FutureProvider(
        // 4
        create: (_) async {
          final service = MockService();
          // 5
          service.create();
          return service;
        },
        lazy: false,
      ),
    ],
    // 6
    child: MaterialApp(
      title: 'Recipes',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.light,
        primaryColor: Colors.white,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MainScreen(),
    ));
}


下面是上面代码中的内容。

  1. MultiProvider使用providers属性来定义多个提供者。
  2. 第一个提供者是你现有的ChangeNotifierProvider
  3. 你添加了一个新的提供者,FutureProvider,它返回一个ProviderFuture
  4. 使create()成为异步的。在这个服务中你不需要它,但你以后会用到它。
  5. 调用create()来加载JSON文件。
  6. 唯一的孩子是一个MaterialApp,像以前一样。

现在,你已经准备好加载和显示模拟食谱了。

加载模拟菜谱

打开ui/recipes/recipe_list.dart,添加MockServiceProvider导入。

import '../../mock_service/mock_service.dart';
import 'package:provider/provider.dart';


_buildRecipeLoader()中,将RecipeService.create()改为。

Provider.of<MockService>(context)


现在看起来应该是这样的。

future: Provider.of<MockService>(context).queryRecipes(
    searchTextController.text.trim(),
    currentStartPosition,
    currentEndPosition),


热重新加载应用程序并在食谱标签中搜索任何术语。注意,无论你输入什么,你都只能得到鸡肉或意大利面的食谱。这是因为MockService只提供这两个结果。在未来,测试特定的变化或添加更多的模拟数据将变得更加容易。

恭喜你,你现在有了一个服务,即使你没有账户或你的网络不工作,也能工作。你甚至可以使用MockService进行测试。这样做的好处是,你知道你会得到什么结果,因为数据被存储在静态JSON文件中。

惊人的工作! 这一章有很多东西需要学习,但这是很重要的工作。状态管理是Flutter开发的一个关键概念。

Provider是状态管理的唯一选择吗?不是。请做好准备,快速浏览一下其他库。

其他状态管理库

还有其他的包可以帮助进行状态管理,并在你的应用程序中管理状态时提供更多的灵活性。虽然Provider为widget树中较低的widget提供了类,但其他包为整个应用提供了更通用的状态管理解决方案,通常可以实现单向的数据流架构。

此类库包括ReduxBLoCMobXRiverpod。下面是对每个库的快速概述。

Redux

如果你来自Web或React开发,你可能对Redux很熟悉,它使用了行动、还原器、视图和存储等概念。其流程看起来像这样。

行动,如在用户界面上的点击或来自网络操作的事件,被发送到还原器,还原器将它们转化为一种状态。该状态被保存在一个商店中,该商店通知监听器,如视图和组件,关于变化。

Redux架构的好处是,视图可以简单地发送动作并等待来自存储的更新。

要在Flutter中使用Redux,你需要两个包。reduxflutter_redux

对于迁移到Flutter的React开发者来说,Redux的一个优势是它已经很熟悉了。如果你对它不熟悉,可能需要花点时间来学习。

BLoC

BLoC是Business Logic Component的缩写。它旨在将UI代码与数据层和业务逻辑分开,帮助你创建易于测试的可重用代码。把它想象成一个事件流:一些小部件提交事件,其他小部件对它们作出反应。BLoC坐在中间,指导对话,利用流的力量。

它在Flutter社区相当流行,并且有很好的文档。

MobX

MobX是从网络世界来到Dart的。它使用了以下概念。

Observables:保持状态。 Actions: 突变状态。 Reactions:对观察变量的变化做出反应。

MobX自带注解,可以帮助你编写代码,使其更加简单。

一个优点是MobX允许你将任何数据包裹在一个可观察变量中。它相对来说比较容易学习,而且需要的生成的代码文件比BLoC要小。

河豚

Provider的作者Remi Rousselet写了Riverpod来解决Provider的一些弱点。事实上,Riverpod是Provider的变形词 Rousselet想解决以下问题。

  1. 消除对Flutter的依赖性,使其可以使用纯Dart代码。
  2. 编译安全。让编译器捕捉到Provider发生的错误。
  3. 拥有更多的功能。
  4. 更加灵活。

Riverpod是相当新的,它看起来是一个很有前途的状态管理包,可以在未来使用。

关键点

  • 状态管理是Flutter开发的关键。
  • Provider是一个很好的包,有助于状态管理。
  • 其他处理应用程序状态的包包括 Redux, Bloc, MobXRiverpod.
  • 存储库是一种提供数据的模式。
  • 通过为存储库提供一个接口,你可以在不同的存储库之间切换。例如,你可以在真实的和模拟的资源库之间切换。
  • 模拟服务是一种提供假数据的方式。

何去何从?

如果你想了解更多关于。

  1. 状态管理,请到flutter.dev/docs/develo…
  2. 对于Flutter Redux,请到pub.dev/packages/fl…
  3. 对于Bloc,请访问bloclibrary.dev/#/
  4. 对于MobX,请访问github.com/mobxjs/mobx…
  5. 对于Riverpod,请访问riverpod.dev/
  6. 关于Clean Architecture,请访问pusher.com/tutorials/c…

在下一章,你将学习所有关于流的知识,以处理可以连续发送和接收的数据。到时见!


www.deepl.com 翻译