Skip to main content

架构设计模式

如果您已经阅读过架构指南页面,或者您熟悉 Flutter 和 MVVM 模式,那么以下文章适合您。

这些文章并非关于高级应用架构,而是关于解决特定设计问题的,这些问题可以改善您的应用程序代码库,无论您是如何构建应用程序的。也就是说,这些文章中的代码示例都假设使用了前面几页中介绍的 MVVM 模式。

在构建用户体验时,性能感知有时与代码的实际性能一样重要。一般来说,用户不喜欢等待操作完成才能看到结果,任何需要超过几毫秒的操作都可能被用户认为是“缓慢的”或“无响应的”。

开发人员可以通过在后台任务完全完成之前呈现成功的UI状态来帮助减轻这种负面感知。一个例子是点击“订阅”按钮,并立即看到它变为“已订阅”,即使对订阅API的后台调用仍在运行。

这种技术被称为乐观状态、乐观UI或乐观用户体验。在这个示例中,您将使用乐观状态并遵循[Flutter 架构指南][]来实现应用程序功能。

示例功能:订阅按钮

#

此示例实现了一个订阅按钮,类似于您可以在视频流应用程序或新闻通讯中找到的按钮。

Application with subscribe button

点击按钮时,应用程序会调用外部API,执行订阅操作,例如在数据库中记录用户现在已在订阅列表中。出于演示目的,您不会实现实际的后端代码,而是会将此调用替换为模拟网络请求的伪操作。

如果调用成功,按钮文本将从“订阅”更改为“已订阅”。按钮背景颜色也会更改。

相反,如果调用失败,按钮文本应恢复为“订阅”,并且UI应向用户显示错误消息,例如使用Snackbar。

遵循乐观状态的思想,按钮应该在点击后立即更改为“已订阅”,并且只有在请求失败时才更改回“订阅”。

Animation of application with subscribe button

功能架构

#

首先定义功能架构。遵循架构指南,在Flutter项目中创建这些Dart类:

  • 一个名为SubscribeButtonStatefulWidget
  • 一个名为SubscribeButtonViewModel的扩展ChangeNotifier的类
  • 一个名为SubscriptionRepository的类
dart
class SubscribeButton extends StatefulWidget {  const SubscribeButton({  super.key,  });   @override <span...  Read full article

大多数 Flutter 应用,无论大小,都需要在某个时刻将数据存储在用户的设备上,例如 API 密钥、用户偏好或应该离线可用的数据。

在本食谱中,您将学习如何在使用推荐的Flutter 架构设计的 Flutter 应用中集成键值数据的持久性存储。如果您完全不熟悉将数据存储到磁盘,您可以阅读在磁盘上存储键值数据食谱。

键值存储通常用于保存简单数据,例如应用配置,在本食谱中,您将使用它来保存暗模式偏好设置。如果您想学习如何在设备上存储复杂数据,您可能需要使用 SQL。在这种情况下,请查看此食谱之后的食谱,名为持久性存储架构:SQL

示例应用:带主题选择的应用

#

示例应用程序包含一个屏幕,顶部有应用栏,底部有项目列表和文本字段输入。

ToDo application in light mode

AppBar中,Switch允许用户在深色和浅色主题模式之间切换。此设置会立即应用,并使用键值数据存储服务存储在设备中。当用户再次启动应用程序时,会恢复此设置。

ToDo application in dark mode

存储主题选择键值数据

#

此功能遵循推荐的 Flutter 架构设计模式,具有表示层和数据层。

  • 表示层包含ThemeSwitch小部件和ThemeSwitchViewModel
  • 数据层包含ThemeRepositorySharedPreferencesService

主题选择表示层

#

ThemeSwitch是一个包含Switch小部件的StatelessWidget。开关的状态由ThemeSwitchViewModel中的公共字段isDarkMode表示。当用户点击开关时,代码会在视图模型中执行命令toggle

dart<pre... Read full article

大多数 Flutter 应用,无论大小,都可能需要在某个时刻将数据存储到用户的设备上。例如,API 密钥、用户偏好或应该离线可用的数据。

在本食谱中,您将学习如何在 Flutter 应用中使用 SQL 集成用于复杂数据的持久性存储,遵循 Flutter 架构设计模式。

要了解如何存储更简单的键值数据,请查看食谱: 持久性存储架构:键值数据

要阅读本食谱,您应该熟悉 SQL 和 SQLite。如果您需要帮助,可以在阅读本食谱之前阅读使用 SQLite 持久化数据 食谱。

此示例使用sqflitesqflite_common_ffi 插件,两者结合起来支持移动端和桌面端。Web 端的支持在实验性插件sqflite_common_ffi_web 中提供,但本示例中未包含。

示例应用:待办事项列表应用

#

示例应用包含一个屏幕,顶部有应用栏,一个项目列表和底部有一个文本字段输入。

ToDo application in light mode

应用程序的主体包含 TodoListScreen。此屏幕包含 ListTile 项目的 ListView,每个项目代表一个待办事项。底部,TextField 允许用户通过编写任务描述然后点击“添加”FilledButton 来创建新的待办事项。

用户可以点击删除 IconButton 来删除待办事项。

待办事项列表使用数据库服务本地存储,并在用户启动应用程序时恢复。

使用 SQL 存储复杂数据

#

此功能遵循推荐的Flutter 架构设计,包含 UI 层和数据层。此外,在领域层中,您将找到使用的数据模型。

... Read full article

离线优先应用程序是一种即使断开互联网连接也能提供大部分或全部功能的应用程序。离线优先应用程序通常依赖于存储的数据,以便为用户提供临时访问那些本来只能在线访问的数据。

一些离线优先应用程序无缝地结合了本地和远程数据,而其他应用程序则会在使用缓存数据时通知用户。同样,一些应用程序在后台同步数据,而另一些则需要用户显式地同步数据。这一切都取决于应用程序的要求和它提供的功能,开发人员需要决定哪种实现最符合他们的需求。

在本指南中,您将学习如何在 Flutter 中实现离线优先应用程序的不同方法,遵循Flutter 架构指南

离线优先架构

#

如通用架构概念指南中所述,存储库充当单一事实来源。它们负责呈现本地或远程数据,并且应该是唯一可以修改数据的地方。在离线优先应用程序中,存储库结合不同的本地和远程数据源,以在单个访问点呈现数据,而与设备的连接状态无关。

此示例使用 UserProfileRepository,这是一个允许您获得和存储具有离线优先支持的 UserProfile 对象的存储库。

UserProfileRepository 使用两种不同的数据服务:一种用于远程数据,另一种用于本地数据库。

API 客户端 ApiClientService 使用 HTTP REST 调用连接到远程服务。

dart
class ApiClientService {  /// 执行 GET 网络请求以获取 UserProfile  Future<UserProfile> getUserProfile() async {  // ···  }   /// 执行 PUT 网络请求以更新 UserProfile  Future<void<span...  Read full article

模型-视图-视图模型 (MVVM) 是一种设计模式,它将应用程序的一个特性分成三个部分:模型、视图模型和视图。视图和视图模型构成了应用程序的 UI 层。存储库和服务表示应用程序的数据层,或 MVVM 的模型层。

命令是一个包装方法的类,它有助于处理该方法的不同状态,例如运行、完成和错误。

视图模型 可以使用命令来处理交互和运行操作。同样,它们可以用来显示不同的 UI 状态,例如操作运行时的加载指示器,或操作失败时的错误对话框。

随着应用程序的增长和功能的扩大,视图模型可能会变得非常复杂。命令可以帮助简化视图模型并重用代码。

在本指南中,您将学习如何使用命令模式来改进您的视图模型。

实现视图模型时的挑战

#

Flutter 中的视图模型类通常通过扩展 ChangeNotifier 类来实现。这允许视图模型在数据更新时调用 notifyListeners() 来刷新视图。

dart
class HomeViewModel extends ChangeNotifier {  // ··· }

视图模型包含 UI 状态的表示,包括正在显示的数据。例如,此 HomeViewModelUser 实例公开给视图。

dart
class HomeViewModel extends ChangeNotifier { <span...  Read full article

Dart 提供了内置的错误处理机制,能够抛出和捕获异常。

错误处理文档中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法不需要声明它们,调用方法也不需要捕获它们。

这可能导致异常未被正确处理的情况。在大项目中,开发人员可能会忘记捕获异常,不同的应用程序层和组件可能会抛出未记录的异常。这可能导致错误和崩溃。

在本指南中,您将了解此限制以及如何使用 result 模式来减轻它。

Flutter 应用中的错误流

#

遵循Flutter 架构指南 的应用程序通常由视图模型、存储库和服务等部分组成。当这些组件中的一个函数失败时,它应该将错误传达给调用组件。

通常,这是通过异常来完成的。例如,API 客户端服务未能与远程服务器通信可能会抛出 HTTP 错误异常。调用组件(例如存储库)必须捕获此异常或忽略它并让调用视图模型处理它。

这可以在以下示例中观察到。考虑以下类:

  • 服务 ApiClientService 执行对远程服务的 API 调用。
  • 存储库 UserProfileRepository 提供由 ApiClientService 提供的 UserProfile
  • 视图模型 UserProfileViewModel 使用 UserProfileRepository

ApiClientService 包含一个方法 getUserProfile,在某些情况下会抛出异常:

  • 如果响应代码不是 200,则该方法会抛出 HttpException
  • 如果响应格式不正确,则 JSON 解析方法会抛出异常。
  • HTTP 客户端可能会由于网络问题而抛出异常。

以下代码测试各种可能的异常:

dart
class ApiClientService {  // ···   Future<UserProfile> getUserProfile()...  Read full article