在上一篇文章中,我们已经看到了如何用Flutter和Firebase建立一个简单的认证流程。
作为其中的一部分,我们创建了三个小部件。
- A
SignInPage, 用于登录用户 - 一个
HomePage, 用于签出用户 - 一个
LandingPage,用于决定显示哪个页面。
而这些都是在下面的widget树中一起组成的。
如图所示,所有这些widget访问FirebaseAuth ,作为一个全局的单子变量。
// SignInPage
await FirebaseAuth.instance.signInAnonymously();
// HomePage
await FirebaseAuth.instance.signOut();
// LandingPage
FirebaseAuth.instance.onAuthStateChanged;
许多第三方库通过单子暴露他们的API,所以像这样使用它们并不罕见。
然而,通过单子的全局访问会导致代码无法测试。
而在这种情况下,我们的小部件就会与FirebaseAuth紧密耦合。
在实践中,这意味着。
- 我们不能写一个测试来验证当签到按钮被按下时是否调用了
signInAnonymously()。 - 我们不能轻易地用不同的认证服务来交换
FirebaseAuth,如果我们想的话。
如果我们想写出干净、可测试的代码,我们需要解决这些问题。
所以在这篇文章中,我们将探讨一个更好的全局访问的替代方案。
而我将在后续文章中展示如何为FirebaseAuth 写一个API包装器。
全局访问与范围访问
全局访问有一个主要问题。我们可以用我们的示例代码来说明这个问题。
class SignInPage extends StatelessWidget {
Future<void> _signInAnonymously() async {
try {
await FirebaseAuth.instance.signInAnonymously();
} catch (e) {
print(e); // TODO: show dialog with error
}
}
...
}
在这里,SignInPage 直接询问了FirebaseAuth 的一个实例。
一个更好的方法是告诉我们的SignInPage 它需要什么,通过传递一个FirebaseAuth 对象给它的构造函数。
class SignInPage extends StatelessWidget {
SignInPage({@required this.firebaseAuth});
final FirebaseAuth firebaseAuth;
Future<void> _signInAnonymously() async {
try {
await firebaseAuth.signInAnonymously();
} catch (e) {
print(e); // TODO: show dialog with error
}
}
...
}
这种方法被称为构造函数注入。
注意:Flutter SDK中的Widget就是这样创建的。参数被传递给构造函数,并在widget中使用。这保证了没有副作用,因为widget是纯粹的组件,只依赖于明确传递的参数。
那么,我们是否应该总是将依赖关系传递给我们的部件的构造函数呢?
没那么快。
Flutter大量地使用构件。
这通常会导致非常嵌套的widget树,并可能导致这种情况的发生。
在这里,我们有三个消费者小部件需要同步访问一个FirebaseUser 对象。
这个对象只在LandingPage 的StreamBuilder 内可用。
为了让我们的消费者部件访问FirebaseUser ,我们将不得不把它注入到所有的中间部件,即使它们不直接需要它。
我经历过这种情况。相信我,这很不好玩。
为了避免这种情况,你可能会想通过调用FirebaseAuth.instance.currentUser() 来获得用户。然而,这是一个异步方法,返回一个Future<FirebaseUser> 。我们不应该需要调用一个异步API来获得一个我们已经检索到的对象。
因此,当我们需要在widget树上传播数据时,构造函数注入并不能很好地扩展。
而且对于需要大量参数的widget来说,它也是非常不实用的。举个例子。
class SomeComplexWidget extends StatelessWidget {
SignInPage({@required this.firebaseAuth, @required this.firestore, @required this.sharedPreferences});
final FirebaseAuth firebaseAuth;
final Firestore firestore;
final SharedPreferences sharedPreferences;
// complex logic here
}
我喜欢大量的模板代码--从来没有人说过。
幸运的是,有一个解决方案。
作用域访问
范围访问是指让对象对整个widget子树可用。
Flutter已经使用了这种技术。如果你曾经调用过Navigator.of(context),MediaQuery.of(context) 或Theme.of(context) ,那么你已经使用了范围访问。
在引擎盖下,这是通过一个特殊的部件实现的,这个部件叫做 InheritedWidget.
虽然你可以基于InheritedWidget ,建立你自己的widget,但这也会导致大量的模板代码。
那么,感谢社区,有一个更好的方法。
进入提供者
这个由Remi Rousselet和Flutter团队开发的库让生活变得更简单。
简而言之,Provider是您的Flutter应用程序的一个依赖注入系统。我们可以用它将任何类型的对象(值、流、模型、BLoCs)暴露给我们的小部件。
所以我们来看看如何使用它。
我们可以将Provider添加到我们的pubspec.yaml 文件中。
// pubspec.yaml
dependencies:
provider: ^3.0.0
我们可以更新我们的例子应用程序来使用它,只需做两个改动。
- 在我们的widget树的顶端添加一个
Provider<FirebaseAuth> - 在需要的地方使用
Provider.of<FirebaseAuth>(context),而不是FirebaseAuth.instance。
注意:作为Provider.of 的替代品,你可以使用Consumer ,它也是 Provider 包的一部分。Flutter官方文档中的这一页详细介绍了它们的用法和区别。
我们的例子应用程序的更新的widget树看起来像这样。
最上面的三个部件(MyApp,Provider 和MaterialApp ),可以像这样组成。
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<FirebaseAuth>(
builder: (_) => FirebaseAuth.instance,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: LandingPage(),
),
);
}
}
然后,我们可以更新我们的LandingPage ,使用Provider 。
class LandingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// retrieve firebaseAuth from above in the widget tree
final firebaseAuth = Provider.of<FirebaseAuth>(context);
return StreamBuilder<FirebaseUser>(
stream: firebaseAuth.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
FirebaseUser user = snapshot.data;
if (user == null) {
return SignInPage();
}
return HomePage();
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
}
}
并对SignInPage 和HomePage 做同样的处理,以使用Provider 。
class SignInPage extends StatelessWidget {
Future<void> _signInAnonymously() async {
try {
// retrieve firebaseAuth from above in the widget tree
final firebaseAuth = Provider.of<FirebaseAuth>(context);
await firebaseAuth.signInAnonymously();
} catch (e) {
print(e); // TODO: show dialog with error
}
}
...
}
class HomePage extends StatelessWidget {
Future<void> _signOut() async {
try {
// retrieve firebaseAuth from above in the widget tree
final firebaseAuth = Provider.of<FirebaseAuth>(context);
await firebaseAuth.signOut();
} catch (e) {
print(e); // TODO: show dialog with error
}
}
...
}
关于BuildContext的注意。我们可以检索到FirebaseAuth 对象,因为我们把一个BuildContext 变量传给了Provider.of<FirebaseAuth>(context) 。你可以把context 看成是一个部件在部件树中的位置。
还记得我说过构造函数注入在深度嵌套的widget树中是不现实的吗?
那么,Provider使对象可以被整个widget子树所访问。
因此,每当你需要在widget树上传播数据时,Provider应该在你的大脑中亮起。💡
总结
在这篇文章中,我们已经了解了作为全局访问替代品的范围访问。
我们还看到了如何使用Provider 作为Flutter的依赖注入系统。
顺便说一下,这只是一个基本介绍。
有许多高级用例,我们需要使多个对象能够被我们的部件访问。一些对象可能有相互依赖性。而且我们可能需要不同种类的提供者。
提供者支持所有这些情况。我将在将来写更多关于它的内容。
现在,你可以在GitHub上看到我是如何使用Flutter和Firebase的参考认证流程的。
如果想更深入地了解Provider以及如何使用它进行状态管理,您可以查看我的Flutter & Firebase课程。
编码愉快!