Flutter Riverpod:如何在应用程序启动期间注册监听器

373 阅读7分钟

你是否曾经需要在应用程序启动后立即注册一个听众?

这方面的例子包括。

在所有这些情况下,我们的目标是。

  • 注册一个流监听器来处理所有传入的事件
  • 运行一些代码来修改应用程序的状态导航到一个特定的页面

例如,你可能已经写了这样的代码来处理来自FirebaseDynamicLinks 的所有传入链接。

void main() async {
  // Normal initialization
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // register a listener to process incoming links
  FirebaseDynamicLinks.instance.onLink.listen((link) {
    // TODO: Handle link
  });
  // run the app
  runApp(const MyApp());
}

虽然这很有效,但如果事件处理代码很复杂的话,它就会失控。

如果你需要一个以上的监听器,你的main() 方法很快就会成为你所有应用程序启动代码的垃圾场。

幸运的是,Riverpod包可以帮助我们,我们可以用它来。

  • 初始化有一个或多个依赖关系的复杂对象(通过读取相应的提供者)。
  • 保持我们的应用程序启动逻辑的整洁和整齐

在这一过程中,我们将学习到一些有用的类,如 ProviderContainerUncontrolledProviderScope等有用的类,并为我们的开发者工具包添加一个新的有价值的技术。🛠

准备好了吗?开始吧!

重新审视应用程序的初始化逻辑

作为一个起点,让我们考虑上面例子中的这段代码。

void main() async {
  ...
  // register a listener to process incoming links
  FirebaseDynamicLinks.instance.onLink.listen((link) {
    // TODO: Handle link
  });
  runApp(const MyApp());
}

把这个放在main方法里面并不理想,尤其是当事件处理逻辑很复杂,我们需要访问额外的依赖关系时。而且我们还应该避免在这里使用FirebaseDynamicLinks.instance singleton。

在我们的代码中直接访问单子有各种缺点,而且有更好的替代方案。欲了解更多信息,请阅读。Flutter中的单子。如何避免它们以及如何替代它们

一个更好的方法是。

  • 将所有的监听器和事件处理逻辑移到一个单独的类中
  • 在应用程序启动时初始化该类

因此,让我们看看如何使用Riverpod包,按照4个步骤来做。

1.创建一个StreamProvider

首先,让我们创建一个StreamProvider ,让我们访问我们需要的流。

final onDynamicLinkProvider = StreamProvider<PendingDynamicLinkData>((ref) {
  // For simplicity, here we use FirebaseDynamicLinks directly.
  // On production codebases we would get the stream from a DynamicLinksRepository.
  return FirebaseDynamicLinks.instance.onLink;
});

为了简单起见,我们直接在提供者内部访问FirebaseDynamicLinks.instance 。但是在一个生产代码库中,我们可以创建一个DynamicLinksRepository ,将FirebaseDynamicLinks 作为构造参数来代替。更多的细节,请阅读我关于资源库模式的文章。

2.创建一个带有事件处理代码的服务类

现在我们有了我们的onDynamicLinkProvider ,我们可以创建一个使用它的服务类。

class DynamicLinksService {
  // 1. Pass a Ref argument to the constructor
  DynamicLinksService(this.ref) {
    // 2. Call _init as soon as the object is created
    _init();
  }
  final Ref ref;

  void _init() {
    // 3. listen to the StreamProvider
    ref.listen<AsyncValue<PendingDynamicLinkData>>(onDynamicLinkProvider,
        (previous, next) {
      // 4. Implement the event handling code
      final linkData = next.value;
      if (linkData != null) {
        debugPrint(linkData.toString());
        // TODO: Handle linkData
      }
    });
  }
}

一些注意事项。

  1. DynamicLinksService 类需要一个Ref 参数,我们可以用它来访问我们可能需要的任何提供者。
  2. 我们在构造函数中立即调用私有的_init 方法。
  3. 我们使用ref.listen 为我们的流注册一个监听器。
  4. 在监听器的回调中,我们可以根据需要处理previousnext 的值。

上面的previousnext 值的类型是AsyncValue<PendingDynamicLinkData> ,因为监听观察一个Stream<T> 总是给我们类型为AsyncValue<T> 的值。如果你对AsyncValue 不熟悉,请阅读。Flutter Riverpod提示。使用AsyncValue而不是FutureBuilder或StreamBuilder

我还应该指出的是。

  • _init 方法是私有的,你添加到这个类的任何其他方法也应该是私有的。这样一来,启动监听器的唯一方法就是创建一个DynamicLinksService 的实例。
  • 我没有包括事件处理代码,因为这是特定的应用。如果你需要访问这个类中的任何其他依赖关系,你可以调用ref.read(someProvider).someMethod()

3.为服务类创建一个提供者

一旦我们有了我们的DynamicLinksService 类,我们就可以创建一个提供者,我们将用它来访问它。

final dynamicLinksServiceProvider = Provider<DynamicLinksService>((ref) {
  return DynamicLinksService(ref);
});

这非常简单,因为我们只需要将ref 参数传递给构造函数。

但是,如果我们现在启动应用程序,DynamicLinksService 里面的代码将不会运行,因为dynamicLinksServiceProvider 只会在我们第一次读取它的时候创建它**(Riverpod提供者是懒于加载的**),而且没有小部件或其他类在使用它。

换句话说:如果我们想使用DynamicLinksService ,我们需要在main() 方法里面初始化它。

4.用ProviderContainer读取服务类的提供者

下面是我们的main() 方法,再一次。

void main() async {
  // Normal initialization
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // TODO: How to initialize our DynamicLinksService?
  // run the app
  runApp(ProviderScope(
    child: const MyApp(),
  ));
}

注意,我们只能用Ref 对象创建一个DynamicLinksService ,而main() 方法没有。🧐

为了解决这个鸡生蛋蛋生鸡的问题🐣,我们需要使用一个ProviderContainer 。 这里是方法。

void main() async {
  // Normal initialization
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // 1. Create a ProviderContainer
  final container = ProviderContainer();
  // 2. Use it to read the provider 
  container.read(dynamicLinksServiceProvider);
  // 3. Pass the container to an UncontrolledProviderScope and run the app
  runApp(UncontrolledProviderScope(
    container: container,
    child: const MyApp(),
  ));
}

然后就可以了!我们现在可以用一个ProviderContainer 读取dynamicLinksServiceProvider ,然后把它作为一个参数传给一个UncontrolledProviderScope

由于提供者将创建DynamicLinksService ,我们的监听器现在将被注册,并在应用程序启动时处理所有传入的事件!🏁


顺便说一下,注意对container.read() 的调用会返回DynamicLinksService 本身。

final dynamicLinksService = container.read(dynamicLinksServiceProvider);

但在这种情况下,我们可以忽略返回值,因为我们不需要它。此外,我们不能对它调用任何方法,因为唯一的公共方法是构造函数(启动监听器)。

ProviderContainer和UncontrolledProviderScope如何工作?

但什么是ProviderContainer ? Riverpod文档将其定义为:

一个存储提供者状态的对象,并允许覆盖一个特定提供者的行为。

它还这样说。

如果你使用Flutter,你不需要关心这个对象(在测试之外),因为它是由ProviderScope 为你隐式创建的。

这条规则的例外情况是,如果我们需要在main() 方法内创建一个接受Ref 参数的对象。在这种情况下,明确地创建一个ProviderContainer 给我们一个 "逃生舱",让我们访问/初始化DynamicLinksService

如果你在ProviderScope 中有任何提供者覆盖,你现在可以把它们移到ProviderContainer

final container = ProviderContainer(
  overrides: [], // list your overrides here
);

结论

我们现在已经知道了如何在应用程序启动时注册一个监听器,同时保持我们的main() 方法的整洁。

再一次,这就是四个步骤。

  1. 创建一个StreamProvider
  2. 创建一个带有处理所有流事件的监听器的服务类
  3. 为该服务类创建一个提供者
  4. ProviderContainer 读取服务类的提供者。main()

如果你有多个服务类,这种方法的扩展性很好,因为你可以用一行代码初始化每个服务。

final container = ProviderContainer();
container.read(authServiceProvider);
container.read(dynamicLinksServiceProvider);
container.read(messagingServiceProvider);
runApp(UncontrolledProviderScope(
  container: container,
  child: const MyApp(),
));

这对那些听众来说是最有用的。

  • 在应用程序运行时始终处于活动状态
  • 独立于用户界面,不针对任何特定的部件

尽管如果你愿意,你可以在你的服务类中添加一个输出StreamValueListenable ,以便UI层中的widget可以观察到它。如果你想显示一个警报或SnackBar ,或在某个事件发生时导航到一个特定的页面,这很有用。


这就是了!我们现在有了一个可重复的过程,以可扩展的方式注册监听器,而不需要用所有的初始化逻辑重载main 方法。🚀