How To Architect A Production-Level App In Flutter: Phone Number Authentication - Part 2

Erkan Sahin
ITNEXT
Published in
8 min readSep 6, 2021

--

In the previous part, we implemented the domain and infrastructure layers. Next, we are going to implement the Application and Presentation layers. The application layer is where state management and business logic are handled. My choice of state management package is flutter_bloc.

Although I love Bloc, it requires a lot of boilerplate code to implement. Fortunately, there is a light version of Bloc called Cubit. It is a subset of Bloc which does not rely on events and instead uses methods to emit new states. If you do not need advanced features like debounce, throttle, switchMap, using a cubit will be more than enough for state management.

Now, we are going to implement a cubit for handling phone number sign-in logic. It uses the authentication service interface in the domain layer to communicate with the Firebase authentication service. It will not explicitly use Firebase in the application layer, which means our cubits will be Firebase independent. We can switch the service provider from Firebase to any other service seamlessly. The reason is that we depend on the abstract IAuthService class rather than a specific service provider.

When the user logs in with a phone number, the interaction is as follows:

The user:

  • enters the phone number
  • presses the next button
  • sees the next button disappear
  • sees a loading screen
  • receives an SMS code
  • enters the SMS code
  • is navigated to the home page
  • receives an error message if anything goes wrong in the flow

The scenario above may help us determine the methods and states of the PhoneNumberSignInCubit. We can deduce that the following four methods are needed:

  • phoneNumberChanged: To keep the most up to date phone number in the state
  • signInWithPhoneNumber : Starts the phone number sign-in process when the user presses the next button
  • smsCodeChanged : To keep the most up to date SMS code in the state
  • verifySmsCode : To verify the SMS code the user entered

When the user interacts with the UI through the methods mentioned above, the state of the cubit will change. The UI reflects the state of the cubit. Considering the user interactions and the methods, the state class should keep the following:

  • phoneNumber: The phone number the user types
  • verificationIdOption: A verification id which will be used to verify the SMS code
  • smsCode: The SMS code the user types
  • isInProgress : A loading state to understand whether there is an ongoing process
  • failureOption :The failure the user encounters if anything goes wrong during the sign-in process

We are done with the most challenging part of state management which is to construct the state machine. Freezed classes are a perfect candidate for implementing state classes as they are immutable, and support value equality. The PhoneNumberSignInState class will look like the following:

Observe that there are three additional getters in the state class. These getters are derived from the existing states and their purpose is to simplify the usage of states in the UI. For instance, the UI does not need to know the existence of the verification id. It is just an implementation detail. The only thing the UI cares about is to know when to show the next button, or when to display the SMS code entry form. So, the getters we defined facilitate the logic in the UI quite a lot.

The state class is ready. We can now jump to the fun part and implement the business logic. Whenever the user types, we need to update the phoneNumber or smsCode in the state accordingly:

When the user presses the Next button, signInWithPhoneNumber method is called. Then, existing errors in the state are cleared and in-progress state is emited. The _phoneNumberSignInSubscription will listen to the signInWithPhoneNumber stream defined in the authentication service interface. If a failure is received during the sign-in process, we emit the state accordingly. If the user receives an SMS code successfully, the verification id is emited to the state to be used in the verifySmsCode method later.

Do not forget to cancel the stream subscription:

Once the user enters the SMS code, verifySmsCode of the cubit is called. This method passes the verificationId and smsCode in the state to the authentication service.

Auth Cubit

The need for AuthCubit springs from the following requirements:

  • When the user opens the app, we need to understand whether the user is already signed in
  • If the user is signed in, the phone number of the user is displayed on the home page
  • If the user is not signed in, the phone number sign-in page is displayed
  • When the user completes sign-in successfully, we redirect the user to the home page
  • Also, there will be a logout button on the home page. If the user signs out, we navigate the user to the phone number sign-in page

The AuthStateclass will be quite straightforward. It will keep two fields:

  • userModel: The user that is currently signed in. An empty user model will be kept if the user is not signed in
  • isUserCheckedFromAuthService: A boolean flag to understand whether we checked the sign-in status of the user at least once throughout the lifetime of the app. When the app is first opened, our default state assumes that the user is not signed in.isUserCheckedFromAuthService is set to trueafter receiving the actual user information from the userChanges stream.

The AuthState class looks like the following:

The AuthCubit methods are also straightforward. _authUserSubscription listens to the authStateChanges stream.

AuthCubit is a singleton which means that it is created once and lives throughout the app lifetime unless it is disposed manually:

Presentation Layer

The presentation layer is where our Widgets reside. It should not contain business logic or any infrastructure code. The presentation layer interacts with the Application layer using the methods in cubits and updates itself depending on state changes.

Also, routing is handled inside the presentation layer. For routing, I use the auto_route package, which reduces the boilerplate. The package setup is already done in the bare-bones template. I advise you to read the official documentation to see the full capabilities of the package.

Providing Auth Cubit

We need to know the authentication state of the user irrespective of the current route. So, the auth cubit will be provided on top of the MaterialApp and live throughout the app lifetime. It allows us to access the authentication state on any screen of the app:

The initial route of the app is LandingPage . Its only responsibility is to determine where to navigate the user. If the user is signed in, the user is navigated to the HomePage . Otherwise, the user is redirected to the PhoneNumberSignInPage .

When the landing page is created, there are two possible cases:

  • Authentication state is ready before the LandingPage is created
  • Authentication state is received after the LandingPage page is built

The first case is handled in the initState of the LandingPage. According to the state of the AuthCubit, the user is redirected to the right route. If the isUserLoggedInis true, we can safely navigate the user to the HomePage. Else, we need to check an additional flag before navigating the user to the SignInPage. isUserCheckedFromAuthServicetells whether the authenticated user information is available in AuthState. If the query has not resulted when initState is called, we should not take action to navigate the user to any route at this point.

In the second case, we navigate the user to the right route when the AuthState is updated. We will add a BlocListener, which will be triggered when the authenticated user information is available. A circular progress indicator is displayed on the screen until the isUserCheckedFromAuthService is updated:

If the user is not signed in, PhoneNumberSignInPage is displayed. It will utilize the PhoneNumberSignInCubit to communicate with the outside world. We should provide the PhoneNumberSignInCubit using BlocProvider to be able to call its methods and access its state:

We need to place a BlocBuilder to update the UI whenever the PhoneNumberSignInState is updated:

Whenever the user modifies the phone number input, phoneNumberChanged method is called so that we keep the most up-to-date input in the state.

If the user presses the Next button, signInWithPhoneNumber method is called, which will start the sign-in process. After the process begins, we will hide the next button and display a loading indicator at the center of the screen.

If the user receives an error during the sign-in process, we should display a toast on the screen which explains the failure. We can use a BlocListener to catch errors. The cubit state is resetted so that the user can try again if something went wrong:

If everything goes right, the user will receive an SMS code and a verification id from Firebase. Receiving verification id will trigger our BlocBuilder; so, SMS code entry form will be displayed in the UI.

When the user modifies the smsCode input, smsCodeChanged method is called so that we keep the most up-to-date input in the state. When the last code digit is typed, verifySmsCode will be triggered.

If the user is successfully verified, we should redirect the user to the HomePage. The behavior can be accomplished by a BlocListener that is triggered when the login state of the user is changed:

On the HomePage, the phone number of the signed-in user and a Logout button are displayed. For displaying the phone number of the user, we can use a BlocBuilder inside the build function of the home page.

As a rule of thumb, it is important to place your bloc builders as deep as possible in the widget tree. Also, they should not be triggered in every state change of your bloc/cubit. It is better to use buildWhen conditions in your BlocBuilder or use BlocSelector to watch the value changes for improving performance of your app. In the scope of this tutorial, using buildWhen conditions may not affect the performance significantly. However, you should be using them in a production-level app:

Pressing the Logout button triggers the signOut method of the AuthCubit:

When the user signs out successfully, we should redirect the user to the phone number sign-in page. To accomplish this behavior, we can place a BlocListener which triggers when the user is not signed in anymore:

That’s it! You have seen the implementation of the phone number sign-in feature in a production-level app. I am aware that it might be difficult to grasp everything at once since there are many packages and concepts involved. I encourage you to analyze what is done in each layer and add a feature to the existing app. I believe adding Google Sign In feature could be a good challenge for you.

This is the same implementation we used in Sponty. It is a video-driven social media app which is used for spontaneous group gatherings.

I hope that this tutorial helps you understand how you should organize your code in a production-level application. Feel free to ask any questions in the comments section.

Thank you for reading, stay tuned!

--

--