[Flutter翻译]Flutter应用程序架构。演示层

378 阅读10分钟

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

如何实现控制器类,以保持业务逻辑,管理小部件状态,并进行交互 w......

在编写Flutter应用程序时,将任何的业务逻辑与UI代码分开是非常重要的。

这使得我们的代码更容易测试更容易推理,而且随着我们的应用程序变得更加复杂,这一点尤其重要。

为了达到这个目的,我们可以使用设计模式,在我们的应用程序的不同组件之间引入关注点的分离

而作为参考,我们可以采用一个分层应用架构,如本图中所表示的那样。

image.png 使用数据层、领域层、应用层和表现层的Flutter应用程序架构。箭头表示各层之间的依赖关系。

我已经在以前的文章中介绍了上面的一些层。

而这一次,我们将专注于表现层,并学习如何使用控制器来。

  • 保存业务逻辑
  • 管理widget的状态
  • 与数据层的存储库进行交互

这种控制器与你在MVVM模式中使用的视图模型相同。如果你以前使用过flutter_bloc,它的作用与立方体相同。

我们将学习StateNotifier类,它是Flutter SDK中ValueNotifier类的替代。

而为了使之更有用,我们将实现一个简单的认证流程作为例子。

准备好了吗?我们开始吧!

注意:本文基于Riverpod版本2.0.0-dev.5(目前是预发布版本)。

一个简单的认证流程

让我们考虑一个非常简单的应用程序,我们可以用它来匿名登录并在两个屏幕之间切换。

image.png 简单的签到流程

而在这篇文章中,我们将重点讨论如何实现。

  • 一个认证,我们可以用它来签入和签出
  • 我们向用户展示的一个签到小工具屏幕
  • 相应的controller类,在两者之间进行调解。

下面是这个具体例子的参考架构的简化版本。

image.png 签到功能的分层架构

你可以在GitHub上找到这个应用的完整源代码。关于它是如何组织的更多信息,请阅读这个。Flutter项目结构:功能优先还是层级优先?

AuthRepository类

作为一个起点,我们可以定义一个简单的**抽象类,它包含三个方法,我们将用它们来登录、退出和检查认证状态。

abstract class AuthRepository {
  // emits a new value every time the authentication state changes
  Stream<AppUser?> authStateChanges();

  Future<AppUser> signInAnonymously();

  Future<void> signOut();
}

在实践中,我们还需要一个实现AuthRepository的具体类。这可以是基于Firebase或任何其他后端。我们甚至可以暂时用一个**的假仓库来实现它。更多细节,请看这篇关于版本库模式的文章。

为了完整起见,我们还可以定义一个简单的AppUser模型类。

/// Simple class representing the user UID and email.
class AppUser {
  const AppUser({required this.uid});
  final String uid;
  // TODO: Add other fields as needed (email, displayName etc.)
}

如果我们使用Riverpod,我们还需要一个`Provider',我们可以用它来访问我们的资源库。

final authRepositoryProvider = Provider<AuthRepository>((ref) {
  // return a concrete implementation of AuthRepository
  return FakeAuthRepository();
});

接下来,让我们专注于签到屏幕。

The SignInScreen widget

假设我们有一个简单的SignInScreen小组件,像这样定义。

import 'package:flutter_riverpod/flutter_riverpod.dart';

class SignInScreen extends ConsumerWidget {
  const SignInScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sign In'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Sign in anonymously'),
          onPressed: () { /* TODO: Implement */ },
        ),
      ),
    );
  }
}

这只是一个简单的Scaffold,中间有一个ElevatedButton

注意,由于这个类扩展了ConsumerWidget,在build()方法中,我们有一个额外的ref对象,我们可以根据需要用来访问提供者。

直接从我们的widget访问AuthRepository

作为下一步,我们可以使用onPressed回调来进行登录,就像这样。

ElevatedButton(
  child: Text('Sign in anonymously'),
  onPressed: () => ref.read(authRepositoryProvider).signInAnonymously(),
)

这段代码通过以下方式工作。

  • 通过调用ref.read(authRepositoryProvider)获得AuthRepository
  • 对其调用 signInAnonymously()方法

这涵盖了快乐的路径(签入成功)。但我们也应该通过以下方式来考虑加载错误状态。

  • 在签到过程中,禁用签到按钮并显示一个加载指示灯
  • 如果由于任何原因调用失败,显示一个 "点心条 "或警告。

The "StatefulWidget +setState" way

解决这个问题的一个简单方法是。

  • 将我们的widget转换成StatefulWidget(或者说,ConsumerStatefulWidget,因为我们使用Riverpod)
  • 添加一些本地变量来跟踪状态的变化
  • 在对setState()的调用中设置这些变量,以触发一个widget的重建
  • 使用它们来更新用户界面

下面是产生的代码可能看起来像。

class SignInScreen extends ConsumerStatefulWidget {
  const SignInScreen({Key? key}) : super(key: key);

  @overrideclass SignInScreen extends ConsumerStatefulWidget {
  const SignInScreen({Key? key}) : super(key: key);

  @override
  ConsumerState<SignInScreen> createState() => _SignInScreenState();
}

class _SignInScreenState extends ConsumerState<SignInScreen> {
  // keep track of the loading state
  bool isLoading = false;

  // call this from the `onPressed` callback
  Future<void> _signInAnonymously() async {
    try {
      // update the state
      setState(() => isLoading = true);
      // sign in
      await ref
          .read(signInScreenControllerProvider.notifier)
          .signInAnonymously();
    } catch (e) {
      // show a snackbar if something went wrong
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(e.toString())),
      );
    } finally {
      // check if we're still on this screen (widget is mounted)
      if (mounted) {
        // reset the loading state
        setState(() => isLoading = false);
      }
    }
  }
  
  ...
}

对于这样一个简单的应用,这可能是可以的。

但是当我们有更复杂的widget时,这种方法就会很快失控,因为我们在同一个widget类中混合了业务逻辑和UI代码

如果我们想在多个widget中一致地处理错误状态的加载,复制粘贴和调整上面的代码是很容易出错的(而且没有什么乐趣)。

相反,最好是将所有这些问题移到一个单独的控制器类中,它可以。

  • 在我们的 "SignInScreen "和 "AuthRepository "之间进行调解。
  • 管理widget的状态
  • 为小组件提供一种方法来观察状态的变化,并因此而重建自己。

image.png 签到功能的分层架构

因此,让我们看看如何在实践中实现它。

一个基于StateNotifier的控制器类

第一步是创建一个StateNotifier子类。

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Create a StateNotifier subclass using AsyncValue<void> as the state
class SignInScreenController extends StateNotifier<AsyncValue<void>> {
  // set the initial value
  SignInScreenController() : super(const AsyncData<void>(null));
}

注意,由于StateNotifier是一个通用的类,我们需要指定我们要使用的状态类的类型

在这种情况下,我们可以选择AsyncValue<void>,因为这允许我们表示三种状态。

  • 默认(未加载)为AsyncData (与AsyncValue.data相同)
  • 加载作为AsyncLoading (与AsyncValue.loading相同)
  • error作为AsyncError (与AsyncValue.error相同)

而且由于StateNotifier需要一个**初始值,我们必须在初始化器列表中调用super

如果你对AsyncValue及其子类不熟悉,请阅读这个。如何在Flutter中用StateNotifier & AsyncValue处理加载和错误状态

实现登录的方法

接下来,让我们添加一个方法,我们可以用它来登录。

// Create a StateNotifier subclass using AsyncValue<void> as our state
class SignInScreenController extends StateNotifier<AsyncValue<void>> {
  SignInScreenController({required this.authRepository})
      // set the initial value
      : super(const AsyncData<void>(null));
  final AuthRepository authRepository;

  Future<void> signInAnonymously() async {
    // set the state to loading
    state = const AsyncLoading<void>();
    // call `authRepository.signInAnonymously` and await for the result
    state = await AsyncValue.guard<void>(
      () => authRepository.signInAnonymously(),
    );
  }
}

一些说明。

  • 我们添加了一个AuthRepository的依赖关系,因为签到需要它。
  • signInAnonymously()中,我们将状态设置为AsyncLoading,这样小部件就可以显示一个加载UI。
  • 然后,我们调用AsyncValue.guard等待结果(将是AsyncDataAsyncError)。

AsyncValue.guardtry/catch的一个方便的替代品。欲了解更多信息,请阅读这个。在你的StateNotifier子类中使用AsyncValue.guard而不是try/catch

作为一个额外的提示,我们可以使用一个方法tear-off来进一步简化我们的代码。

// pass authRepository.signInAnonymously directly using tear-off
state = await AsyncValue.guard<void>(authRepository.signInAnonymously);

这就完成了我们控制器类的实现,只用了10行代码。

class SignInScreenController extends StateNotifier<AsyncValue<void>> {
  SignInScreenController({required this.authRepository})
      : super(const AsyncData<void>(null));
  final FakeAuthRepository authRepository;

  Future<void> signInAnonymously() async {
    state = const AsyncLoading<void>();
    state = await AsyncValue.guard<void>(authRepository.signInAnonymously);
  }
}

创建一个StateNotifierProvider

接下来,让我们创建一个StateNotifierProvider,我们将在widget类中使用。

final signInScreenControllerProvider =
    // StateNotifierProvider takes the controller class and state class as type arguments
    StateNotifierProvider.autoDispose<SignInScreenController, AsyncValue<void>>(
        (ref) {
  return SignInScreenController(
    authRepository: ref.watch(authRepositoryProvider),
  );
});

值得注意的是,StateNotifierProvider需要两个类型参数。

  • 我们的StateNotifier子类的类型(SignInScreenController)
  • 我们的状态类的类型(AsyncValue<void>)。

我们还使用autoDispose修改器来确保提供者的状态在不再需要时被**处理。

我们可以通过调用ref.watch(authRepositoryProvider)轻松获得authRepository的依赖性。

是时候回到我们的widget类中,把所有的东西都连接起来了!

在widget类中使用我们的控制器

下面是一个更新的SignInScreen版本,它使用了我们新的SignInScreenController类。

class SignInScreen extends ConsumerWidget {
  const SignInScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // watch and rebuild when the state changes
    final AsyncValue<void> state = ref.watch(signInScreenControllerProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sign In'),
      ),
      body: Center(
        child: ElevatedButton(
          // conditionally show a CircularProgressIndicator if the state is "loading"
          child: state.isLoading
              ? const CircularProgressIndicator()
              : const Text('Sign in anonymously'),
          // disable the button if the state is loading
          onPressed: state.isLoading
              ? null
              // otherwise, get the notifier and sign in
              : () => ref
                  .read(signInScreenControllerProvider.notifier)
                  .signInAnonymously(),
        ),
      ),
    );
  }
}

注意在build()方法中,我们观察我们的提供者,并在状态改变时重建widget。

onPressed回调中,我们读取提供者的notifier并调用signInAnonymously()。 我们还可以使用isLoading属性,在签到过程中有条件地禁用按钮。

我们快完成了,只剩下一件事要做。

监听状态变化

就在构建方法的顶部,我们可以添加这个。

@override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<AsyncValue>(
    signInScreenControllerProvider,
    (_, state) {
      if (!state.isRefreshing && state.hasError) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(state.error.toString())),
        );
      }
    },
  );
  ...
}

我们可以用这段代码在状态改变时调用一个监听器回调。

这对于在签到时发生错误时显示错误警告或SnackBar很有用。

注意:自从Riverpod版本2.0.0-dev.1以来,当提供者发出一个AsyncError,后面是AsyncDataAsyncLoading,我们仍然可以读取之前的错误数据。因此,我们可以检查isRefreshing标志,以避免显示多个黑框/错误。

奖励:一个AsyncValue扩展方法

上面的监听器代码相当有用,我们可能想在多个小部件中重复使用它。

要做到这一点,我们可以定义这个`AsyncValue'扩展

extension AsyncValueUI on AsyncValue {
  void showSnackbarOnError(BuildContext context) {
    if (!isRefreshing && hasError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(error.toString())),
      );
    }
  }
}

然后,在我们的widget中,我们可以直接导入我们的扩展并调用这个。

ref.listen<AsyncValue>(
  signInScreenControllerProvider,
  (_, state) => state.showSnackbarOnError(context),
);

结论

通过实现一个基于 "StateNotifier "的自定义控制器类,我们已经了我们的业务逻辑与UI代码的分离

因此,我们的widget类现在是完全无状态的,只关心。

  • 观察状态变化,并根据结果进行重建(使用ref.watch)。
  • 通过调用控制器中的方法来响应用户的输入(使用ref.read)。
  • 听取状态变化并在出错时显示错误(用ref.listen)。

同时,我们控制器的工作是。

  • 代表widget与资源库对话
  • 在需要时发出状态变化

由于控制器不依赖任何UI代码,它可以很容易地进行单元测试

这使得它成为存储任何部件特定业务逻辑的理想场所。

应用层

在更复杂的应用程序中,你可能会遇到这样的用例。

  • 多个小部件共享相同的逻辑
  • 我们需要在同一个方法中与多个资源库对话

这些情况可以更好地描述为应用特定的业务逻辑,它不与任何特定的widget相联系。

而我们可以在这个架构图中属于应用层的服务**类中保存这些逻辑。

image.png 使用数据层、领域层、应用层和表现层的Flutter应用程序架构。箭头表示各层之间的依赖关系

但这将是后续文章的主题。👌

新的Flutter课程现在可用

我推出了一个全新的课程,深入地涵盖了许多重要的主题,包括应用架构Riverpod的状态管理自动测试

在5月10日之前,我将以**40%的折扣提供这个课程。


www.deepl.com 翻译