In the previous articles we have seen how to create a simple authentication flow with firebase_auth
and the Provider package:
- Super Simple Authentication Flow with Flutter & Firebase
- Flutter: Global Access vs Scoped Access with Provider
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 course.