Flutter|全局访问与带提供者的范围访问

101 阅读6分钟

上一篇文章中,我们已经看到了如何用Flutter和Firebase建立一个简单的认证流程。

作为其中的一部分,我们创建了三个小部件。

  • ASignInPage, 用于登录用户
  • 一个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 对象。

这个对象只在LandingPageStreamBuilder 内可用。

为了让我们的消费者部件访问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,ProviderMaterialApp ),可以像这样组成。

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(),
            ),
          );
        }
      },
    );
  }
}

并对SignInPageHomePage 做同样的处理,以使用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课程。

编码愉快!

参考资料