Flutter: Global Access vs Scoped Access with Provider

In the previous article we have seen how to build a simple authentication flow with Flutter & Firebase.

As part of this, we created three widgets:

  • A SignInPage, used to sign in the user
  • A HomePage, used to sign out the user
  • A LandingPage, used to decide which page to show

And these are composed together in the following widget tree:

As the diagram shows, all these widgets access FirebaseAuth as a global singleton variable:

// SignInPage await FirebaseAuth.instance.signInAnonymously(); // HomePage await FirebaseAuth.instance.signOut(); // LandingPage FirebaseAuth.instance.onAuthStateChanged;

Many 3rd party libraries expose their APIs via singletons, so it is not uncommon to see them used like this.

However, global access via singletons leads to code that is not testable.

And in this case, our widgets become tightly coupled to FirebaseAuth.

In practice, this means that:

  • we cannot write a test to verify that signInAnonymously() is called when the sign-in button is pressed.
  • we can't easily swap FirebaseAuth with a different authentication service if we want to.

And we need to address these issues if we want to write clean, testable code.

So in this article we will explore a better alternative to global access.

And I will show how to write an API wrapper for FirebaseAuth in a follow-up article.

Global Access vs Scoped Access

There is one main problem with global access. And we can use our example code to illustrate this:

class SignInPage extends StatelessWidget { Future<void> _signInAnonymously() async { try { await FirebaseAuth.instance.signInAnonymously(); } catch (e) { print(e); // TODO: show dialog with error } } ... }

Here, the SignInPage asks for an instance of FirebaseAuth directly.

A better approach would be to tell our SignInPage what it needs, by passing a FirebaseAuth object to its constructor:

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 } } ... }

This approach is called constructor injection.

Note: Widgets in the Flutter SDK are created this way. Arguments are passed to the constructor, and used inside the widget. This guarantees that there are no side effects, because widgets are pure components that only depend on arguments that are passed explicitly.


So, should we always pass dependencies to the constructors of our widgets?

Not so fast.

Flutter uses composition heavily.

This often results in very nested widget trees, and can lead to this scenario:

Here, we have three consumer widgets that require synchronous access to a FirebaseUser object.

This object is only available inside the StreamBuilder of the LandingPage.

In order for our consumer widgets to access FirebaseUser, we would have to inject it to all intermediate widgets, even if they don't need it directly.

I've been there. Trust me, it's not fun.

To avoid this, you may be tempted to get the user with a call to FirebaseAuth.instance.currentUser(). However, this is an asynchronous method that returns a Future<FirebaseUser>. And we shouldn't need to call an async API to get an object that we already retrieved.


So constructor injection doesn't scale well when we need to propagate data down the widget tree.

And it is also very impractical with widgets that require a lot of arguments. Example:

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 }

I love a lot of boilerplate code - nobody ever said.

Luckily, there is a solution.

Scoped Access

Scoped access is about making objects available to an entire widget sub-tree.

Flutter already uses this technique. If you ever called Navigator.of(context), MediaQuery.of(context) or Theme.of(context), then you have used scoped access.

Under the hood, this is implemented with a special widget called InheritedWidget.

And while you could build your own widgets based on InheritedWidget, this can also lead to a lot of boilerplate code.

Well, thanks to the community, there is a better way.

Enter Provider

This library by Remi Rousselet and the Flutter team makes life easier.

In a nutshell, Provider is a dependency injection system for your Flutter apps. We can use it to expose objects of any kind (values, streams, models, BLoCs) to our widgets.

So let's see how to use it.


We can add Provider to our pubspec.yaml file:

// pubspec.yaml dependencies: provider: ^3.0.0

And we can update our example app to use it, by making two changes:

  • Add a Provider<FirebaseAuth> to the top of our widget tree
  • Use Provider.of<FirebaseAuth>(context) where needed, instead of FirebaseAuth.instance.

Note: As an alternative to Provider.of, you could use Consumer, which is also part of the Provider package. This page on the official Flutter documentation covers their usage and differences in detail.

The updated widget tree for our example app looks like this:

The top three widgets (MyApp, Provider and MaterialApp), can be composed like this:

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

Then, we can update our LandingPage to use 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(), ), ); } }, ); } }

And do the same with the SignInPage and HomePage to use 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 } } ... }

Note about BuildContext: We can retrieve the FirebaseAuth object because we pass a BuildContext variable to Provider.of<FirebaseAuth>(context). You can think of context as the position of a widget in the widget tree.


Remember when I said that constructor injection is impractical with deeply nested widget trees?

Well, Provider makes objects accessible to an entire widget sub-tree.

So every time you need to propagate data down the widget tree, Provider should light-up in your brain. 💡

Wrap up

In this article we have learned about scoped access as an alternative to global access.

And we have seen how to use Provider as a dependency injection system for Flutter.

By the way, this was just a basic introduction.

There are many advanced use cases where we need to make multiple objects accessible to our widgets. Some objects may have inter-dependencies. And we may need different kinds of providers.

Provider supports all these cases. I will write more about it in the future.

For now, you can see how I'm using it in my Reference Authentication Flow with Flutter & Firebase on GitHub:

And for a more in-depth coverage of Provider and how to use it for state management, you can check my Flutter & Firebase course.

Happy coding!

References

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.