Flutter: Designing an Authentication API with Service Classes

Posted by Andrea Bizzotto on June 17, 2019

In the previous articles we have seen how to create a simple authentication flow with firebase_auth and the Provider package:

These techniques are the basis for my Reference Authentication Flow with Flutter & Firebase.

We will now see how use service classes to encapsulate 3rd party libraries and APIs, and decouple them from the rest of the application. We will use authentication as a concrete example of this.

TL;DR:

  • Write a service class as an API wrapper that hides any implementation details.
  • This includes API methods with all their input and output (return) arguments.
  • (optional) create a base abstract class for the service class, so that it’s easier to swap this with a different implementation

Problem Statement

In the previous article, we used this code to sign in the user with FirebaseAuth:

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

Here we use Provider.of<FirebaseAuth>(context) to retrieve an instance of FirebaseAuth.

This avoids the usual problems with global access (see my previous article about global access vs scoped access for more details).

However, we are still accessing the FirebaseAuth API directly in our code.

This can lead to some problems:

  • How to deal with breaking changes in future versions of FirebaseAuth?
  • What if we want to swap FirebaseAuth with a different auth provider in the future?

Either way, we would have to update or replace calls to FirebaseAuth across our codebase.


And once our project grows, we may add a lot of additional packages. These may include shared preferences, permissions, analytics, local authentication, to name a few common ones.

This multiplies the effort required to maintain our code as APIs change.

Solution: create service classes

A service class is simply a wrapper.

Here is how we can create a generic authentication service based on FirebaseAuth:

class User {
  const User({@required this.uid});
  final String uid;
}

class FirebaseAuthService {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

  // private method to create `User` from `FirebaseUser`
  User _userFromFirebase(FirebaseUser user) {
    return user == null ? null : User(uid: user.uid);
  }

  Stream<User> get onAuthStateChanged {
    // map all `FirebaseUser` objects to `User`, using the `_userFromFirebase` method
    return _firebaseAuth.onAuthStateChanged.map(_userFromFirebase);
  }

  Future<User> signInAnonymously() async {
    final user = await _firebaseAuth.signInAnonymously();
    return _userFromFirebase(user);
  }

  Future<void> signOut() async {
    return _firebaseAuth.signOut();
  }
}

In this example, we create a FirebaseAuthService class that implements the API methods that we need from FirebaseAuth.

Note how we have created a simple User class, which we use in the return type of all methods in the FirebaseAuthService.

This way, the client code doesn’t depend on the firebase_auth package at all, because it can work with User objects, rather than FirebaseUser.


With this setup, we can update our top-level widget to use the new service class:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<FirebaseAuthService>(
      builder: (_) => FirebaseAuthService(),
      child: MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.indigo,
        ),
        home: LandingPage(),
      ),
    );
  }
}

And then, we can replace all calls to Provider.of<FirebaseAuth>(context) with Provider.of<FirebaseAuthService>(context).

As a result, all our client code no longer needs to import this line:

import 'package:firebase_auth/firebase_auth.dart';

This means that if any breaking changes are introduced in firebase_auth, any compile errors can only appear inside our FirebaseAuthService class.

As we add more and more packages, this approach makes our app more maintainable.


As an additional (and optional) step, we could also define an abstract base class:

abstract class AuthService {
  Future<User> signInAnonymously();
  Future<void> signOut();
  Stream<User> get onAuthStateChanged;
}

class FirebaseAuthService implements AuthService {
  ...
}

With this setup, we can use the base class type when we create and use our Provider, while creating an instance of the subclass in the builder:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<AuthService>( // base class
      builder: (_) => FirebaseAuthService(), // concrete subclass
      child: MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.indigo,
        ),
        home: LandingPage(),
      ),
    );
  }
}

Creating a base class is extra work though. It may only be worth when we know that we need to have multiple implementations at the same time.

If this is not the case, I recommend writing only one concrete service class. And since modern IDEs make refactoring tasks easy, you can rename the class and its usages without pain if needed.


For reference, I’m using a base AuthService class with two implementations in my Reference Authentication Flow project.

This is so that I can toggle between a Firebase and a mock authentication service at runtime, which is useful for testing and demo purposes.

Showtime

Here is a video showing all these techniques in practice, using my Reference Authentication Flow as an example:

Conclusion

Service classes are a good way of hiding the implementation details of 3rd party code in your app.

They can be particularly useful when you need to call an API method in multiple places in your codebase (analytics and logging libraries are a good example of this).

In a nutshell:

  • Write a service class as an API wrapper that hides any implementation details.
  • This includes API methods with all their input and output (return) arguments.
  • (optional) create a base abstract class for the service class, so that it’s easier to swap this with a different implementation

Happy coding!

Source code

The example code from this article was taken from my Reference Authentication Flow with Flutter & Firebase:

In turn, this project complements all the in-depth material from my Flutter & Firebase Udemy course. This is available for early access at this link (discount code included):

LEARN FLUTTER TODAY

Sign up for updates and get my free Flutter Layout Cheat Sheet.

No Spam. Ever. Unsubscribe at any time.