[Flutter翻译]Widget-Async-Bloc-Service:一个用于Flutter应用程序的实用架构

177 阅读14分钟

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

本文介绍了一种新的架构模式,我经常在我的Flutter应用程序中使用。它的灵感是......

困难程度。中级/高级。所有观点都是我自己的。

简介

状态管理是目前Flutter的一个热门话题。

在过去的一年里,各种状态管理技术被提出。

Flutter团队和社区(尚未)确定一个单一的 "通用 "解决方案。

这是有道理的,因为不同的应用程序有不同的要求。而选择最合适的技术取决于我们要建立的东西。

说实话,一些状态管理技术已经被证明是非常流行的。

  • Scoped Model以其简单性而闻名。
  • BLoCs也被广泛使用,它们与Streams和RxDart一起很好地用于更复杂的应用程序。
  • 最近在Google I/O上,Flutter团队向我们展示了如何使用Provider包和ChangeNotifier来传播小部件间的状态变化。

有多种选择可以是一件好事。

但它也可能让人困惑。选择一种能够随着我们的应用程序的增长而正常工作和扩展的技术是很重要的。

更重要的是,在早期做出正确的选择可以为我们节省大量的时间和精力

我对状态管理和应用程序架构的看法

在过去的一年里,我一直在构建大量的Flutter应用,有大有小。

在这段时间里,我遇到并解决了很多问题。

而且我还了解到,在状态管理方面没有银弹。

然而,经过一次又一次的构建和拆解,我已经微调了一种技术,在我所有的项目中都能很好地工作。

因此,在这篇文章中,我介绍了一种新的架构模式,它是由我定义的。

  • 借用了很多现有模式的想法
  • 调整它们以适应现实世界中Flutter应用程序的需要

在我们看到这个模式是什么样子的之前,让我定义一些目标。

这个模式应该。

  • 一旦明确了基本的构建模块,就应该***容易理解
  • 在添加新功能时,容易复制
  • 建立在干净的架构原则之上
  • 在编写反应式的Flutter应用程序时能很好地工作。
  • 要求很少或没有模板代码
  • 导致可测试的代码
  • 导致可移植的代码
  • 倾向于小型、可组合的部件和类
  • 易于与异步API(Futures和Streams)集成
  • 随着应用程序的规模和复杂性的增加,可以很好地扩展

在Flutter现有的状态管理技术中,这种模式最主要的是建立在BLoCs上,并且与RxVMS架构相当相似。

不多说了,我很高兴地介绍一下。

Widget-Async-BLoC-Service模式

简称。WABS(这很酷,因为它包含了我名字的首字母 :D)

这种架构模式有四种变体。

Widget-Bloc-Service

image.png

Widget-Service

image.png

Widget-Bloc

image.png

只有小工具

image.png

注意:除了Widget项目外,BLoC和Service项目都是可选的项目。

换句话说:你可以根据具体情况使用或省略它们

现在,让我们通过一个更详细的图来探索完整的实现。

image.png

首先,这张图定义了三个应用层

  • 用户界面层:这始终是必要的,因为它是我们的小部件所在的地方。
  • 数据层(可选):这是我们添加逻辑和修改状态的地方。
  • 服务层(可选):这是我们用来与外部服务通信的地方。

接下来,让我们为每个层能(和不能)做什么定义一些规则。

游戏规则

UI层

这就是我们放置小部件的地方。

小工具可以是无状态的,也可以是有状态的,但它们不应该包括任何明确的状态管理逻辑。

显式**状态管理的一个例子是Flutter计数器的例子,当增量按钮被按下时,我们用setState()增加计数器。

一个隐式状态管理的例子是一个StatefulWidget,它包含一个TextField,由TextEditingController管理。在这种情况下,我们需要一个StatefulWidget,因为TextEditingController引入了副作用(我发现了这一点 the hard way),但是我们没有明确地管理任何状态。


UI层的小部件可以自由地调用由块或服务定义的同步async方法,并可以通过StreamBuilder订阅流。

请注意上图是如何将一个小部件与BLoC的输入和输出都连接起来的。但是我们可以使用这种模式将一个部件连接到输入,并将另一个部件连接到输出。

image.png

换句话说,我们可以实现一个生产者→消费者的数据流。


WABS模式鼓励我们将任何状态管理逻辑转移到数据层。所以我们来看看。

数据层

在这一层,我们可以定义本地全球的应用状态,以及修改状态的代码。

这是用业务逻辑组件(BLoC)完成的,这是DartConf 2018期间首次引入的模式。

BLoC的构思是为了将业务逻辑与UI层分开,并增加跨平台的代码重用。

当使用BLoC模式时,widget可以。

  • 事件分派到一个汇中
  • 通过一个被通知**状态的更新

根据最初的定义,我们只能通过汇和流与BLoCs进行通信。


虽然我喜欢这个定义,但我发现它在一些用例中的限制性太强。所以在WABS中,我使用了一种BLoC的变体,叫做Async BLoC

就像BLoCs一样,我们有可以被订阅的输出流。

然而,BLoC的输入可以包括一个异步水槽,一个异步方法,或者两者都是。

换句话说,我们从这个。

image.png

到这个。

image.png

非同步方法可以。

  • 将零、一个或多个值添加到输入汇中。
  • 返回一个带有结果的Future<T>。调用代码可以 "等待 "结果并做相应的事情。
  • 抛出一个异常。调用代码可以用try/catch来检测,如果需要,可以显示一个警告。

稍后,我们将看到一个完整的例子,说明这在实践中是多么有用。


关于BLoCs的更多信息

一个异步BLoC可以定义一个 "StreamController"/"Stream "对,如果使用RxDart,则可以定义相应的 "BehaviorSubject"/"Observable"。

如果我们愿意,我们甚至可以进行高级流操作,比如用combineLatest组合流。为了明确起见。

  • 如果需要以某种方式组合,我建议在一个BLoC中拥有多个流。

  • 我不鼓励在一个BLoC中使用多个`StreamController'。相反,我更倾向于将代码分割成两个或更多的BLoC类,以便更好地分离问题。

我们应该/不应该在数据层/BLoCs中做的事情

  • BLoCs应该只包含纯Dart代码。没有UI代码,没有导入Flutter文件,或在BLoCs中使用BuildContext
  • BLoCs不应该直接调用第三方代码。这是服务类的工作。
  • Widgets和BLoCs之间的接口与BLoCs和服务之间的接口相同。也就是说,BloCs可以通过sync/async方法与服务类直接通信,并通过流来通知更新。

服务层

服务类与BLoCs具有相同的输入/输出接口。

然而,服务和BLoCs之间有一个基本区别。

  • BLoCs可以持有和修改状态。
  • 服务不能。

换句话说,我们可以把服务看作是纯粹的,功能组件。

它们可以修改和转换从第三方库接收的数据。

举个例子。Firestore服务

  • 我们可以实现一个 "FirestoreDatabase "服务,作为Firestore的特定领域API-wrapper。
  • Data in (read): 这将Firestore文档中的键值对流转换为强类型的不可更改的数据模型。
  • Data out(写)。这将数据模型转换回键值对,并写入Firestore。

在这种情况下,服务类执行简单的数据操作。与BLoCs不同,它不持有任何状态。

关于术语的说明。其他文章在提到与第三方库对话的类时使用了Repository这个术语。甚至连Repository模式的定义也随着时间的推移而发生了变化(更多信息见本文)。在这篇文章中,我没有明确区分服务和存储库的区别。

把东西放在一起:提供者包

一旦我们定义了我们的BLoCs和服务,我们就需要将它们提供给我们的小部件。

一段时间以来,我一直在使用Remi Rousselet的[provider]包。这是一个完整的Flutter的依赖注入系统,基于InheritedWidget

我非常喜欢它的简单性。下面是如何使用它来添加一个认证服务。

return Provider<AuthService>(
  builder: (_) => FirebaseAuthService(), // FirebaseAuthService implements AuthService
  child: MaterialApp(...),
);

这就是我们如何使用它来创建一个BLoC。

return Provider<SignInBloc>(
  builder: (_) => SignInBloc(auth: auth),
  dispose: (_, bloc) => bloc.dispose(),
  child: Consumer<SignInBloc>(
    builder: (_, bloc, __) => SignInPage(bloc: bloc),
  ),
);

注意Provider部件如何接受一个可选的dispose回调。我们用它来处理BLoCs并关闭相应的StreamControllers。

Provider给了我们一个简单而灵活的API,我们可以用它来添加任何我们想要的东西到我们的widget树上。它与BLoCs、服务、价值和更多的东西一起工作,非常好。

image.png

我将在接下来的文章中更详细地介绍如何使用Provider。现在,我强烈推荐谷歌I/O的这个演讲。


一个真实世界的例子:签到页

现在我们已经看到了WABS在概念上是如何工作的,让我们用它来构建一个带有Firebase认证的签到流程。

下面是我的Reference Authentication Flow with Flutter & Firebase中的一个交互样本。

一些观察结果。

  • 当签到被触发时,我们禁用所有的按钮,并显示一个 "CircularProgressIndicator"。我们设置了一个 "加载 "状态为 "true "来做这个。
  • 当签到成功或失败时,我们重新启用所有按钮并恢复标题文本。我们设置loading=false来做这件事。
  • 当签到失败时,我们呈现一个警告对话框。

下面是用来驱动这个逻辑的SignInBloc的简化版本。

import 'dart:async';
import 'package:firebase_auth_demo_flutter/services/auth_service.dart';
import 'package:meta/meta.dart';

class SignInBloc {
  SignInBloc({@required this.auth});
  final AuthService auth;

  final StreamController<bool> _isLoadingController = StreamController<bool>();
  Stream<bool> get isLoadingStream => _isLoadingController.stream;

  void _setIsLoading(bool isLoading) => _isLoadingController.add(isLoading);

  Future<void> signInWithGoogle() async {
    try {
      _setIsLoading(true);
      return await auth.signInWithGoogle();
    } catch (e) {
      rethrow;
    } finally {
      _setIsLoading(false);
    }
  }

  void dispose() => _isLoadingController.close();
}

注意这个BLoC的公共API只暴露了一个Stream和一个Future

Stream<bool> get isLoadingStream;
Future<void> signInWithGoogle();

这与我们对Async BLoC的定义是一致的。

所有的魔法都发生在signInWithGoogle()方法中。所以让我们用注释再次回顾一下。

Future<void> signInWithGoogle() async {
  try {
    // first, add loading = true to the stream sink
    _setIsLoading(true);
    // then sign-in and await for the result
    return await auth.signInWithGoogle();
  } catch (e) {
    // if sign in failed, rethrow the exception to the calling code
    rethrow;
  } finally {
    // on success or failure, add loading = false to the stream sink
    _setIsLoading(false);
  }
}

就像普通的BLoC一样,这个方法将值添加到一个sink中。

但除此之外,它还可以异步地返回一个值,或者抛出一个异常。

这意味着我们可以在我们的 "SignInPage "中编写这样的代码。

// called by the `onPressed` callback of our button
Future<void> _signInWithGoogle(BuildContext context) async {
  try {
    await bloc.signInWithGoogle();
    // handle success
  } on PlatformException catch (e) {
    // handle error (show alert)
  }
}

这段代码看起来很简单。它应该是这样的,因为我们在这里需要的是async/awaittry/catch

然而,这在BLoC的 "严格 "版本中是不可能的,它只使用一个水槽和一个流。作为参考,在Redux中实现这样的东西是...呃...不好玩的

Async-BLoC看起来是对BLoC的一个小改进,但它带来了所有的不同


关于处理异常的说明

顺便说一下,另一种可能的处理异常的方法是在流中添加一个错误对象,像这样。

Future<void> signInWithGoogle() async {
  try {
    // first, add loading = true to the stream sink
    _setIsLoading(true);
    // then sign-in and await for the result
    return await auth.signInWithGoogle();
  } catch (e) {
    // add error to the stream
    _isLoadingController.addError(e);
  } finally {
    // on success or failure, add loading = false to the stream sink
    _setIsLoading(false);
  }
}

然后,在widget类中,我们可以写这样的代码。

class SignInPage extends StatelessWidget {
  SignInPage({@required this.bloc});
  final SignInBloc bloc;
  
  // called by the `onPressed` callback of our button
  Future<void> _signInWithGoogle(BuildContext context) async {
    await bloc.signInWithGoogle();
  }
   
  void build(BuildContext context) {
    return StreamBuilder(
      stream: isLoadingStream,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // show error
          showDialog(...);
        }
        // build UI based on snapshot
      }
    )
  }
}

然而,这是很糟糕的,有两个原因。

  • 它在 "StreamBuilder "的构建器中显示一个对话框。这不是很好,因为构建器只应该返回一个widget,而不是执行任何命令性的代码。
  • 这段代码缺乏清晰性。我们显示错误的地方和我们签到的地方完全不同。

所以,不要这样做,如上所示,使用try/catch

继续前进...

Can we use WABS to create an Async-Service?

当然可以。正如我之前所说。

  • BLoCs可以保持和修改状态。
  • 服务不可以。

然而,它们面向公众的API也遵守同样的规则。

下面是一个数据库API的服务类的例子。

abstract class Database {
  // CRUD operations for Job
  Future<void> setJob(Job job);
  Future<void> deleteJob(Job job);
  Stream<List<Job>> jobsStream();

  // CRUD operations for Entry
  Future<void> setEntry(Entry entry);
  Future<void> deleteEntry(Entry entry);
  Stream<List<Entry>> entriesStream({Job job});
}

我们可以使用这个API来向/从Cloud Firestore写入和读取数据。

调用代码可以定义这个方法,向数据库写入一个新的`Job'。

Future<void> _submit(Job job) async {
  try {
    await database.setJob(job);
    // handle success
  } on PlatformException catch (e) {
    // handle error (show alert) 
  }
}

同样的模式,非常简单的错误处理。

与RxVMS比较

在这篇文章中,我介绍了Widget-Async-LoC-Service,作为Flutter中现有架构模式的一种改编。

WABS与Thomas Burkhart的[RxVMS模式](www.burkharts.net/apps/blog/r…

各个层之间甚至有密切的匹配。

image.png

两者之间的主要区别在于。

  • WABS使用Provider包,而RxVMS使用GetIt服务定位器
  • WABS使用简单的async方法来处理UI事件,而RxVMS使用RxCommand

RxCommand是一个强大的抽象,用于处理用户界面事件和更新。它删除了创建StreamController/Stream对BLoCs所需的模板代码。

然而,它确实带来了一个更大的学习曲线。对于我的用例来说,Async-Bloc可以完成工作,而且更简单,尽管有一点额外的模板。

我还喜欢WABS不需要任何外部库就可以实现(除了Provider包)。

最终选择哪一个取决于你的用例,但也取决于个人的偏好和品味。

[我应该在我的应用程序中使用BLoCs吗?](#should-i-use-blocs-in-my-apps?)

BLoCs有一个陡峭的学习曲线。为了理解它们,你还需要熟悉流和StreamBuilder

当使用流时,需要考虑各种因素。

  • 流的连接状态是什么?(none, waiting, active, done)
  • 该流是单一还是多个订阅?
  • StreamControllerStreamSubscription总是需要被处理的
  • 处理嵌套的 "StreamBuilder "会导致Flutter重建widget树时出现棘手的调试问题。

所有这些都给我们的代码增加了开销。

当更新本地应用程序的状态时(例如,将状态从一个widget传播到另一个),有比BLoC更简单的替代方案。我计划在后续文章中写到这一点。

在任何情况下,我发现在用Firestore构建实时应用程序时,BLoC非常有效,因为数据通过流从后端流入应用程序。

在这种情况下,结合流或用RxDart进行转换是很常见的。BLoCs是做这个的好地方。

结语

这篇文章是对WABS的深入介绍,这种架构模式我已经在多个项目中使用了一段时间。

说实话,随着时间的推移,我一直在完善它,在写这篇文章之前,我甚至都没有为它起一个名字。

正如我之前所说,架构模式只是完成我们工作的工具。我的建议是:选择对你和你的项目更有意义的工具。

如果你在你的项目中使用WABS,让我知道它对你的作用。

编码愉快!

源码

本文的示例代码取自我的Reference Authentication Flow with Flutter & Firebase。

反过来,这个项目补充了我的Flutter & Firebase课程中的所有深入材料。

参考资料


www.deepl.com 翻译