你是否曾经需要在应用程序启动后立即注册一个听众?
这方面的例子包括。
- 倾听来自
FirebaseMessaging包 - 侦听来自的动态链接
FirebaseDynamicLinks包 - 倾听来自包的认证状态变化
FirebaseAuth包的状态变化
在所有这些情况下,我们的目标是。
- 注册一个流监听器来处理所有传入的事件
- 运行一些代码来修改应用程序的状态或导航到一个特定的页面
例如,你可能已经写了这样的代码来处理来自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包可以帮助我们,我们可以用它来。
- 初始化有一个或多个依赖关系的复杂对象(通过读取相应的提供者)。
- 保持我们的应用程序启动逻辑的整洁和整齐
在这一过程中,我们将学习到一些有用的类,如 ProviderContainer和 UncontrolledProviderScope等有用的类,并为我们的开发者工具包添加一个新的有价值的技术。🛠
准备好了吗?开始吧!
重新审视应用程序的初始化逻辑
作为一个起点,让我们考虑上面例子中的这段代码。
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
}
});
}
}
一些注意事项。
DynamicLinksService类需要一个Ref参数,我们可以用它来访问我们可能需要的任何提供者。- 我们在构造函数中立即调用私有的
_init方法。 - 我们使用
ref.listen为我们的流注册一个监听器。 - 在监听器的回调中,我们可以根据需要处理
previous和next的值。
上面的
previous和next值的类型是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() 方法的整洁。
再一次,这就是四个步骤。
- 创建一个
StreamProvider - 创建一个带有处理所有流事件的监听器的服务类
- 为该服务类创建一个提供者
- 用
ProviderContainer读取服务类的提供者。main()
如果你有多个服务类,这种方法的扩展性很好,因为你可以用一行代码初始化每个服务。
final container = ProviderContainer();
container.read(authServiceProvider);
container.read(dynamicLinksServiceProvider);
container.read(messagingServiceProvider);
runApp(UncontrolledProviderScope(
container: container,
child: const MyApp(),
));
这对那些听众来说是最有用的。
- 在应用程序运行时始终处于活动状态
- 独立于用户界面,不针对任何特定的部件
尽管如果你愿意,你可以在你的服务类中添加一个输出Stream 或ValueListenable ,以便UI层中的widget可以观察到它。如果你想显示一个警报或SnackBar ,或在某个事件发生时导航到一个特定的页面,这很有用。
这就是了!我们现在有了一个可重复的过程,以可扩展的方式注册监听器,而不需要用所有的初始化逻辑重载main 方法。🚀