Flutter: Global Access vs Scoped Access with Provider

Flutter: Global Access vs Scoped Access with Provider

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;

global-access-scoped-access-singletons.dart

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

global-access-scoped-access-sign-in-page.dart

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

global-access-scoped-access-sign-in-page-injection.dart

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
}

global-access-scoped-access-many-arguments.dart

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](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html).

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

global-access-scoped-access-main-provider.dart

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

global-access-scoped-access-landing-page-provider.dart

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

global-access-scoped-access-sign-in-home-page-provider.dart

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 Udemy course. This is available for early access at this link (discount code included):

Happy coding!

References

30s ad

Flutter - SQLite Database - CRUD - SUPER EASY!

Flutter & Dart Development For Building iOS and Android Apps

The Complete Flutter App Development Course for Android, iOS

The Complete Flutter and Firebase Developer Course

Dart 2 Complete Bootcamp - Go Hero from Zero in Dart Flutter

flutter mobile-apps

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Google's Flutter 1.20 stable announced with new features - Navoki

Google has announced new flutter 1.20 stable with many improvements, and features, enabling flutter for Desktop and Web

Top 25 Flutter Mobile App Templates in 2020

Flutter has been booming worldwide from the past few years. While there are many popular mobile app development technologies out there, Flutter has managed to leave its mark in the mobile application development world. In this article, we’ve curated the best Flutter app templates available on the market as of July 2020.

How To Succeed In Mobile App Wireframe Design?

This article covers everything about mobile app wireframe design: what to do and what not, tools used in designing a mobile or web app wireframe, and more.

How much does it cost to make a Flutter app for your business?

Get a Free Quote on Android App Development, iPhone App Development, Ionic App Development, Video Development, ASO, SEO, Google Ads/Adwords, SEO for your app Idea.

Flutter – A Boon to Mobile App Developers

Flutter app development is in trend; ever since flutter - the mobile app development framework, has been helping app developers to build amazing applications.