9  comments

Validating data and making illegal states unrepresentable is our second nature after the previous part. But, you know, having these nice ValueObjects by themselves is quite pointless. Let's start taking steps toward implementing the application layer responsible for taking raw email and password Strings from the UI to the authentication backend.

This post is a part of a series. See all the parts ? here ?

Domain abstraction

Before we can venture out into the application layer, we have to create an abstraction for FirebaseAuth and GoogleSignIn in the domain layer. Why? First of all, we want to do everything we can to make Firebase into an implementation detail. Secondly, we're doing this to fulfill the DDD spec where the application layer cannot depend on classes from the infrastructure layer.

If you need a quick refresher to how the overall architecture looks like, here you go:

Which actions do we need to perform on the authentication backend? There are three things, which we will translate into methods.

  1. Register with email and password
  2. Sign in with email and password
  3. Sign in with Google

Although we're a long way from actually implementing these methods, we can create an abstraction right now and postpone its implementation for later. This will allow us to focus on the BLoCs in the application layer before jumping to implement all things Firebase in the bottom-most infrastructure layer.

Let us, therefore, create i_auth_facade.dart under domain/auth. Its methods will take in EmailAddress and Password value objects we created in the previous part. The "i" means interface - although Dart offers only abstract classes which may or may not have an implementation, we can overcome this language design with strong naming conventions.

Facade is a design pattern for connecting two or more classes with weird interfaces into just one simplified interface. In our case, it will connect FirebaseAuth and GoogleSignIn.

i_auth_facade.dart

abstract class IAuthFacade {
  Future<void> registerWithEmailAndPassword({
    @required EmailAddress emailAddress,
    @required Password password,
  });
  Future<void> signInWithEmailAndPassword({
    @required EmailAddress emailAddress,
    @required Password password,
  });
  Future<void> signInWithGoogle();
}
Facades are on the same level as repositories in the layer diagram. They still deal with raw data from data sources and they have the additional role of simplifying the interface of the contained classes.

Although you may think that the code above is perfectly fine, I find it deeply disturbing. How are we going to handle exceptions? As you can see on the diagram above, we want the classes on the repository level to catch all Exceptions and return them as Failures.

AuthFailure

We already know how to create Failures. We currently have two ValueFailures which are used within the EmailAddress and Password value objects to indicate invalid data. AuthFailure will also be a freezed union and it will indicate issues from the IAuthFacade.

Let's think about the possible failures which can occur during authentication. Granted, knowing at least a bit about the exceptions thrown by FirebaseAuth may be helpful here but it's also possible to come up with these failures separately. Things that prohibit successful authentication include:

  1. User "taps out" of the 3rd party sign-in flow (Google in our case)
  2. There is an error on the auth server
  3. User wants to register with an email which is already in use
  4. User enters an invalid combination of email and password

Let's create this union inside domain/auth/auth_failure.dart.

auth_failure.dart

part 'auth_failure.freezed.dart';

@freezed
abstract class AuthFailure with _$AuthFailure {
  const factory AuthFailure.cancelledByUser() = CancelledByUser;
  // Serves as a "catch all" failure if we don't know what exactly went wrong
  const factory AuthFailure.serverError() = ServerError;
  const factory AuthFailure.emailAlreadyInUse() = EmailAlreadyInUse;
  const factory AuthFailure.invalidEmailAndPasswordCombination() =
      InvalidEmailAndPasswordCombination;
}

Returning failures

How can these failures be returned from the IAuthFacade? The naive way would be to return Future<AuthFailure> from the methods. This, however, would force us to use null whenever a failure doesn't occur. As you know, nulls stink!

It's much better to use our trusted friend called Either where we pass around failures inside the Left side of it. But what are we going to put into the Right side? Can we create something like Either<AuthFailure, void>?

No. Dart's void is only a keyword, not a type, and it cannot be used as a generic type parameter. Swift's Void is actually a type, so you can use it in generics. Kotlin uses Unit which is also a regular type. Thankfully, the dartz package provides us, Dart programmers, with a Unit type too!

There is much to be said about Unit but for our purposes, we can think of it as of a functional equivalent to Dart's dumb void keyword. If you're a category theory mathematician, please, calm down ?

Let's update the IAuthFacade interface.

i_auth_facade.dart

abstract class IAuthFacade {
  Future<Either<AuthFailure, Unit>> registerWithEmailAndPassword({
    @required EmailAddress emailAddress,
    @required Password password,
  });
  Future<Either<AuthFailure, Unit>> signInWithEmailAndPassword({
    @required EmailAddress emailAddress,
    @required Password password,
  });
  Future<Either<AuthFailure, Unit>> signInWithGoogle();
}

This return type is perfect because we can easily use it even for methods which do return a value. If, for example, a method doesn't return void (a.k.a. Unit), we'd return Either<AuthFailure, SomeType>.

Ready for the sign-in form

We have created an interface inside the domain layer which will allow us to implement the application logic without any FirebaseAuth dependencies. We've also thought through the different AuthFailures that can occur regardless of the authentication backend we're using. Lastly, we've discovered the Unit type which allows us to return "nothing" inside the Either union.

All of this means that we're now ready to start implementing the sign-in form. We're not going to create any buttons and text fields just yet, instead, we'll hop into the application layer to work on the SignInFormBloc.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

  • Great stuff!

    It would be good to include the build runner command in the written tutorials 2 and 3. Right now they are mentioned only in the videos for those two tutorials and I’m sure that some people don’t use the video or (me) start with the written tutorial then watch. Plus, a cut and paste of the command would be nice!

    Thanks, and keep up the good work!
    Wayne

  • Thank you for this tutorial…but you said you will update the code in future, and you haven’t…

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >