This codelab is part of the Advanced Android in Kotlin course. You'll get the most value out of this course if you work through the codelabs in sequence, but it is not mandatory. All the course codelabs are listed on the Advanced Android in Kotlin codelabs landing page.

Introduction

By allowing users to log in and create an identity within your app, you can provide them with more ways to interact with the app.

With personalized accounts, users can customize their in-app experience, engage with other users, and have their data persisted and transferred if they're using the app on another device (such as web or a new phone).

In this codelab, you will learn the basics of how to support login for your app using the FirebaseUI library. This library makes it easier to build a login flow, and it handles the work of managing user accounts for you.

What you should already know

What you'll learn

What you'll do

Learn more about LiveData and ViewModel

For the app in this codelab, you need a basic understanding of LiveData and ViewModel. Read through the LiveData and ViewModel overviews if you want a brief overview of these concepts.

You can also go through the Developing Android Apps with Kotlin course to learn about fundamental Android topics that you'll encounter as part of this codelab. That course is available as both a Udacity course and a codelabs course.

In this codelab, you'll build an app that displays fun facts about Android. More importantly, the app will have a Login/Logout button. When the user is logged into the app, any displayed Android fact will include a greeting for the user to add a touch of personalization.

Download the sample app, you can either:

Download Zip

... or clone the GitHub repository from the command line by using the following command and switch to the start branch of the repo:

$  git clone https://github.com/googlecodelabs/android-kotlin-login

Important: Since you will be integrating the app to use Firebase, the starter app requires some setup in order for it to build and run. You'll be doing that in the next step of the codelab.

Step 1: Create a Firebase project

Before you can add Firebase to your Android app, you need to create a Firebase project to connect to your Android app.

  1. In the Firebase console, click Add project.
  2. Select or enter a Project name. You can name your project anything, but try to pick a name relevant to the app you're building.
  3. Click Continue.
  4. You can skip setting up Google Analytics and chose the Not Right Now option.
  5. Click Create Project to finish setting up the Firebase project.

Step 2: Register your app with Firebase

Now that you have a Firebase project, you can add your Android app to it.

  1. In the center of the Firebase console's project overview page, click the Android icon to launch the setup workflow.
  2. Enter your app's application ID in the Android package name field. Make sure you enter the ID your app is using, since you cannot add or modify this value after you've registered your app with your Firebase project.
  1. An application ID is sometimes referred to as a package name.
  2. Find this application ID in your module (app-level) Gradle file, usually app/build.gradle (example ID: com.yourcompany.yourproject).
  3. Enter the debug signing certificate SHA-1. You can generate this key by entering the following command in your command line terminal.
keytool -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v -storepass android
  1. Click Register app.

Step 3: Add the Firebase configuration file to your project

Add the Firebase Android configuration file to your app:

  1. Click Download google-services.json to obtain your Firebase Android config file (google-services.json).
  1. Move your config file into the module (app-level) directory of your app.

Step 4: Configure your Android project to enable Firebase products

  1. To enable Firebase products in your app, add the google-services plugin to your Gradle files.
  1. In your root-level (project-level) Gradle file (build.gradle), add rules to include the Google Services plugin. Check that you have Google's Maven repository, as well.

build.gradle

buildscript {

  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
  }

  dependencies {
    // ...

    // Add the following line:
    classpath 'com.google.gms:google-services:4.3.0'  // Google Services plugin
  }
}

allprojects {
  // ...

  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
    // ...
  }
}
  1. In your module (app-level) Gradle file (usually app/build.gradle), add a line to the bottom of the file.

app/build.gradle

apply plugin: 'com.android.application'

android {
  // ...
}

// Add the following line to the bottom of the file:
apply plugin: 'com.google.gms.google-services'  // Google Play services Gradle plugin

Step 4: Add Firebase dependency

In this codelab the main reason for integrating Firebase is to have a way to create and manage users. For this, you need to add a Firebase library that enables you to implement login/logout functionality.

  1. Add the following dependency in your build.gradle (Module:app) file so that you can use the SDK in your app. The firebase-auth SDK allows management of authenticated users of your application.

app/build.gradle:

implementation 'com.firebaseui:firebase-ui-auth:5.0.0'
  1. Sync your project with gradle files to make sure that all dependencies are available to your app. If not prompted, select File > Sync Project with Gradle Files in Android Studio, or from the toolbar.

Step 5: Run the app and inspect the code

Run the app on an emulator or physical device to ensure that your environment is successfully set up to start development.

If successful, you should see the home screen display a fun fact about Android and a login button on the top left corner. Tapping the login button doesn't do anything just yet.

At a high level, this is a single activity app with multiple fragments. The MainFragment contains all the UI you see on the screen below. (You'll be working with the LoginFragment and the SettingsFragment in a followup codelab.)

Familiarize yourself with the code. In particular, notice the following:

  1. FirebaseUserLiveData is the class you will be implementing in order to observe the current Firebase user associated with the app. You will use the FirebaseAuth instance as an entry point to get this user information in a later step.
  2. The MainFragment is tied to the LoginViewModel. LoginViewModel is the class you will include an authenticationState variable of type FirebaseUserLiveData. MainFragment can then observe the value of this authenticationState variable to update the UI accordingly.

In this step you'll use the Firebase Console to set up the authentication methods you want your app to support. For this codelab, you will focus on letting users login with an email address they provide or their Google account.

  1. Navigate to the Firebase console. (Note: If you are still in the Add Firebase workflow, click the X in the top left corner to return to the console.
  2. Select your project, if you are not already in your project.
  3. Open the left-hand navigation and select Develop > Authentication.

  1. Select the Sign-in method tab on the top navigation bar.

  1. Click on the Email/Password row.
  2. In the popup, toggle the Enabled switch. Leave the Email link switch disabled.
  3. Click Save.
  4. Back in the Sign-in method tab of the Authentication window, Click on the Google row.
  5. Toggle the Enabled switch, enter a Project support email, and click Save.

In this task you'll implement the login feature for your users.

  1. Open MainFragment.kt. Notice that the authBbutton object is currently not set up to handle any user input.
  2. In onViewCreated(), implement setOnClickListener for authButton to call launchSignInFlow():

MainFragment.kt

binding.authButton.setOnClickListener { launchSignInFlow() }
  1. Look for the launchSignInFlow() method in MainFragment.kt. It currently contains a TODO.
  2. Complete the launchSignInFlow() function as shown below.

MainFragment.kt

private fun launchSignInFlow() {
   // Give users the option to sign in / register with their email or Google account.
   // If users choose to register with their email,
   // they will need to create a password as well.
   val providers = arrayListOf(
       AuthUI.IdpConfig.EmailBuilder().build(), AuthUI.IdpConfig.GoogleBuilder().build()

       // This is where you can provide more ways for users to register and 
       // sign in.
   )

   // Create and launch the sign-in intent.
   // We listen to the response of this activity with the
   // SIGN_IN_REQUEST_CODE.
   startActivityForResult(
       AuthUI.getInstance()
           .createSignInIntentBuilder()
           .setAvailableProviders(providers)
           .build(),
       SIGN_IN_REQUEST_CODE
   )
}

This allows users to register and sign in with their email address or Google account. If the user chooses to register with their email address, the email and password combination they create is unique for your app. That means they will be able to login to your app using this email address and password combination. However, this doesn't mean that they can also log in to any other Firebase-supported app using the same credentials.

  1. In MainFragment.kt, you can listen for the result of the sign-in process by implementing the onActivityResult() method, as shown below. Since you started the sign in process with SIGN_IN_REQUEST_CODE, you can also listen to the result of the sign in process by filtering for when SIGN_IN_REQUEST_CODE is passed back to onActivityResult(). Start by having some log statements to know whether the user has signed in successfully:

MainFragment.kt

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   if (requestCode == SIGN_IN_REQUEST_CODE) {
       val response = IdpResponse.fromResultIntent(data)
       if (resultCode == Activity.RESULT_OK) {
           // User successfully signed in.
           Log.i(TAG, "Successfully signed in user ${FirebaseAuth.getInstance().currentUser?.displayName}!")
       } else {
           // Sign in failed. If response is null, the user canceled the
           // sign-in flow using the back button. Otherwise, check
           // the error code and handle the error.
           Log.i(TAG, "Sign in unsuccessful ${response?.error?.errorCode}")
       }
   }
}

Your app should now be able to handle registering and logging in users!

  1. Run the app and verify that tapping on the Login button brings up the login screen.
  2. You can now sign in with your email address and a password, or with your Google account.
  3. There will be no change in the UI after you login (you'll implement updating the UI in the next step), but if everything is working correctly, you should see the log message Successfully signed in user ${your name}! after you go through the registration flow.
  4. You can also check that your app now has one registered user into the Firebase console. Navigate to Develop > Authentication, then open the Users tab.

In this task, you'll implement the logic needed to update the UI based on the authentication state. When the user is logged in, you can personalize their home screen by displaying their name. You will also change the Login button to be a Logout button when the user is logged in.

  1. Open the FirebaseUserLiveData.kt class, which has already been created for you. The first thing you need to do is provide a way for other classes in the app to know when a user has logged in or out.
  2. Since you are using the FirebaseAuth library, you can keep track of changes to the logged in user using the FirebaseUser.AuthStateListener callback that's implemented for you as part of the FirebaseUI library. This callback gets triggered whenever a user logs in or out of your app.
  3. Notice that FirebaseUserLiveData.kt defines the authStateListener variable. You will use this variable to store the value of the LiveData. The authStateListener variable was created so that you can properly start and stop listening to changes in the auth state based on the state of your application. For example, if the user puts the app into the background, then the app should stop listening to auth state changes in order to prevent any potential memory leaks.
  4. Update authStateListener so that the value of your FirebaseUserLiveData corresponds to the current Firebase user.

FirebaseUserLiveData.kt

private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
   value = firebaseAuth.currentUser
}
  1. Open LoginViewModel.kt.
  2. In LoginViewModel.kt, create an authenticationState variable based off of the FirebaseUserLiveData object that you just implemented. By creating this authenticationState variable, other classes can now query for whether a user is logged in or not through the LoginViewModel.

LoginViewModel.kt

val authenticationState = FirebaseUserLiveData().map { user ->
   if (user != null) {
       AuthenticationState.AUTHENTICATED
   } else {
       AuthenticationState.UNAUTHENTICATED
   }
}
  1. Open MainFragment.kt.
  2. In MainFragment.kt's observeAuthenticationState(), you can use the authenticationState variable that you just added in LoginViewModel and change the UI accordingly. If there is a logged-in user, authButton should display Logout.

MainFragment.kt

private fun observeAuthenticationState() {
   val factToDisplay = viewModel.getFactToDisplay(requireContext())

   viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
       when (authenticationState) {
           LoginViewModel.AuthenticationState.AUTHENTICATED -> {
               binding.authButton.text = getString(R.string.logout_button_text)
               binding.authButton.setOnClickListener {
                   // TODO implement logging out user in next step
               }

                // TODO 2. If the user is logged in, 
                 // you can customize the welcome message they see by
                 // utilizing the getFactWithPersonalization() function provided

           }
           else -> {
               // TODO 3. Lastly, if there is no logged-in user, 
                // auth_button should display Login and
                //  launch the sign in screen when clicked.
           }
       }
   })
}
  1. If the user is logged in, you can also customize the welcome message they see by utilizing the getFactWithPersonalization() function provided in MainFragment.

MainFragment.kt

// TODO 2. If the user is logged in...
binding.welcomeText.text = getFactWithPersonalization(factToDisplay)
  1. Lastly, if there is no logged-in user (when authenticationState is anything other than LoginViewModel.AuthenticationState.AUTHENTICATED), authButton should display Login and launch the sign-in screen when clicked. There should also be no personalization of the message displayed.

MainFragment.kt

// TODO 3. Lastly, if there is no logged-in user...
binding.authButton.text = getString(R.string.login_button_text)
binding.authButton.setOnClickListener { launchSignInFlow() }
binding.welcomeText.text = factToDisplay

With all the steps completed, your final observeAuthenticationState() method should look similar to the code below.

MainFragment.kt

private fun observeAuthenticationState() {
   val factToDisplay = viewModel.getFactToDisplay(requireContext())

   viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
        // TODO 1. Use the authenticationState variable you just added 
         // in LoginViewModel and change the UI accordingly.
       when (authenticationState) {
            // TODO 2.  If the user is logged in, 
             // you can customize the welcome message they see by
             // utilizing the getFactWithPersonalization() function provided
           LoginViewModel.AuthenticationState.AUTHENTICATED -> {
               binding.welcomeText.text = getFactWithPersonalization(factToDisplay)
               binding.authButton.text = getString(R.string.logout_button_text)
               binding.authButton.setOnClickListener {
                   // TODO implement logging out user in next step
               }
           }
           else -> {
                // TODO 3. Lastly, if there is no logged-in user, 
                 // auth_button should display Login and
                 // launch the sign in screen when clicked.
               binding.welcomeText.text = factToDisplay

               binding.authButton.text = getString(R.string.login_button_text)
               binding.authButton.setOnClickListener {
                   launchSignInFlow()
               }
           }
       }
   })
}
  1. Run your app. The UI should update according to whether a user is logged in or not. If everything is working properly, and you are logged in, the home screen should now greet you by your name in addition to displaying an Android fact. The Login button should also now display Logout.

In this task, you'll implement the logout feature.

Since the app allows users to log in, it should also provide them with a way to log out. Here's an example of how to log out a user with just one line of code:

AuthUI.getInstance().signOut(requireContext())
  1. Open MainFragment.kt.
  2. In MainFragment.kt's observeAuthenticationState(), add the logout logic so that the authButton functions correctly when there is a logged in user. The final result of the method looks like the code below.

MainFragment.kt

private fun observeAuthenticationState() {
   val factToDisplay = viewModel.getFactToDisplay(requireContext())

   viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
       when (authenticationState) {
           LoginViewModel.AuthenticationState.AUTHENTICATED -> {
               binding.welcomeText.text = getFactWithPersonalization(factToDisplay)

               binding.authButton.text = getString(R.string.logout_button_text)
               binding.authButton.setOnClickListener {
                   AuthUI.getInstance().signOut(requireContext())
               }
           }
           else -> {
               binding.welcomeText.text = factToDisplay

               binding.authButton.text = getString(R.string.login_button_text)
               binding.authButton.setOnClickListener {
                   launchSignInFlow()
               }
           }
       }
   })
}
  1. Run the app.
  2. Tap the Logout button and verify that the user is logged out, and the button's text changes to Login.

You can find the final version of the completed app in the master branch of the following repository: https://github.com/googlecodelabs/android-kotlin-login.

In this codelab, you learned:

This codelab covered the basics of how to support login for an Android app.

In this codelab, you allowed users to register and sign in with their email address. However, with the FirebaseUI library you can also support other authentication methods, such as signing in with a phone number. To learn more about the capabilities of the FirebaseUI library and how to utilize other functionalities it provides, check out the following resources:

For more about best practices around login, check out these other resources:

Codelabs:

Android developer documentation:

Videos:

For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.