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:
SignInPage, used to sign in the user
HomePage, used to sign out the user
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:
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
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
FirebaseAuthwith 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:
SignInPage asks for an instance of
A better approach would be to tell our
SignInPage what it needs, by passing a
FirebaseAuth object to its constructor:
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
This object is only available inside the
StreamBuilder of the
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:
I love a lot of boilerplate code — nobody ever said.
Luckily, there is a solution.
Scoped access is about making objects available to an entire widget sub-tree.
Flutter already uses this technique. If you ever called
Theme.of(context), then you have used scoped access.
Under the hood, this is implemented with a special widget called
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.
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
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
Provider.of<FirebaseAuth>(context)where needed, instead of
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 (
MaterialApp), can be composed like this:
Then, we can update our
LandingPage to use
And do the same with the
HomePage to use
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. 💡
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 Udemy course. This is available for early access at this link (discount code included):