Lesson Tuesday

Users can now create accounts and logout from the MainActivity. However, there are still several ways we could improve user experience. In this lesson, we'll explore creating dialogs to communicate when our app is loading, validation methods to prevent errors before they occur, and personalized welcome messages to greet returning users.

Validating Registration Credentials

What if a user attempts to register with an invalid email address? Or a really terrible single-character password? Let's add validation methods to ensure our users' credentials are valid and reasonably secure, and error handling if they are not.

We'll add the following to CreateAccountActivity:

CreateAccountActivity.java
...
    private boolean isValidEmail(String email) {
        boolean isGoodEmail =
                (email != null && android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches());
        if (!isGoodEmail) {
            mEmailEditText.setError("Please enter a valid email address");
            return false;
        }
        return isGoodEmail;
    }

    private boolean isValidName(String name) {
        if (name.equals("")) {
            mNameEditText.setError("Please enter your name");
            return false;
        }
        return true;
    }

    private boolean isValidPassword(String password, String confirmPassword) {
        if (password.length() < 6) {
            mPasswordEditText.setError("Please create a password containing at least 6 characters");
            return false;
        } else if (!password.equals(confirmPassword)) {
            mPasswordEditText.setError("Passwords do not match");
            return false;
        }
        return true;
    }
...

Here, we've defined three methods to validate three different pieces of user-provided data:

  • isValidEmail() uses an Android Pattern (essentially a regular expression built directly into Android) to confirm that the user's email is a valid email address. It does this by checking that the address is in the correct format. If it is not, it displays an error in the mEmailEditText.

  • isValidName() verifies that the name field of our registration form has not been left blank. If it has, it displays an error in mNameEditText.

  • isValidPassword() confirms that the user's password is at least 6 characters long, and that the password and password confirmation fields match. If not, it displays an error in mPasswordEditText. Even though Firebase already requires passwords be at least 6 characters long, we include our own validation method to confirm the password length, so that we can display an error message to our user if necessary.

We'll call these methods in our existing createNewUser() method. This will ensure user credentials are accurate before we create an account with Firebase:

CreateAccountActivity.java
...
public void createNewUser() {
        final String name = mNameEditText.getText().toString().trim();
        final String email = mEmailEditText.getText().toString().trim();
        String password = mPasswordEditText.getText().toString().trim();
        String confirmPassword = mConfirmPasswordEditText.getText().toString().trim();

        boolean validEmail = isValidEmail(email);
        boolean validName = isValidName(name);
        boolean validPassword = isValidPassword(password, confirmPassword);
        if (!validEmail || !validName || !validPassword) return;

        mAuth.createUserWithEmailAndPassword(email, password).addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {

            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {

                if (task.isSuccessful()) {
                    Log.d(TAG, "Authentication successful");
                } else {
                    Toast.makeText(CreateAccountActivity.this, "Authentication failed.",
                            Toast.LENGTH_SHORT).show();
                }

            }

        });
    }
...

If all information is valid, we create a new user in Firebase. If it is not, the return in the line if (!validEmail || !validName || !validPassword) return; will halt our createNewUser() method, and the validation method(s) will display errors.

Progress Dialogs

As technology becomes faster and faster, we expect increasingly speedy responses from our apps and devices. When they don't immediately do what we expect, we can't help but wonder "is it broken?" Many apps now display small loading animations to reassure users that their application isn't frozen; it's just still loading!

You've already had some experience with Dialogs earlier in Android. ProgressDialogs are a different flavour of Dialog. ProgressDialog displays an animated progress indicator along with a customizable message. Let's add a ProgressDialog object to our CreateAccountActivity that informs the user when our app is in the process of authenticating their account.

We'll begin by adding the following code to CreateAccountActivity:

CreateAccountActivity.java
public class CreateAccountActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    private ProgressDialog mAuthProgressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        createAuthProgressDialog();
    }

    private void createAuthProgressDialog() {
        mAuthProgressDialog = new ProgressDialog(this);
        mAuthProgressDialog.setTitle("Loading...");
        mAuthProgressDialog.setMessage("Authenticating with Firebase...");
        mAuthProgressDialog.setCancelable(false);
    }

   ...
     private void createNewUser() {
        ...
        if (!validEmail || !validName || !validPassword) return;

        mAuthProgressDialog.show();

        mAuth.createUserWithEmailAndPassword(email, password)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {

                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {

                        mAuthProgressDialog.dismiss();

                        if (task.isSuccessful()) {
                            Log.d(TAG, "Authentication successful");
                        } else {
                            Toast.makeText(CreateAccountActivity.this, "Authentication failed.",
                                    Toast.LENGTH_SHORT).show();
                        }
                    }
                });
     ...

Let's walk through what functionality this code handles:

  • First, we add a new member variable for our ProgressDialog.

  • Inside of onCreate(), we call a new method createAuthProgressDialog(), which is defined below onCreate().

  • In createAuthProgressDialog(), we set the title and message values of the dialog box, and setCancelable() to false so that users cannot close the dialog manually. (We want this dialog box to remain in sight until the account is either successfully authenticated, or we have errors to display to the user).

  • Finally, we show the ProgressDialog in our createNewUser() method with the line mAuthProgressDialog.show();. Notice that this line is only called after the form validation methods have returned true.

  • When the createUserWithEmailAndPassword() method is complete (no matter what the outcome is), we dismiss the dialog entirely with the line mAuthProgressDialog.dismiss(); so that the user may either continue using the app, or view any error messages.

Now, if we launch our application and create a new account, we should see a progress dialog appear while Firebase creates and authenticates our account. Pretty cool!

Greet User by Name

Many apps and websites include personal touches when users log in. The most common is simply greeting the user by name. How welcoming! Let's do the same in MyRestaurants.

Gathering and Setting Names

To do this we'll need to save the name value from the user's registration form to their Firebase account. We'll start by saving it as a member variable, so we can access it throughout CreateAccountActivity:

CreateAccountActivity.java
public class CreateAccountActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    private String mName;
    ...

    private void createNewUser() {
        mName = mNameEditText.getText().toString().trim();
        ...
        boolean validName = isValidName(mName);
        ...
    }
    ...
}

Firebase does not collect user names by default, but as described in their Managing Users documentation, they do offer methods to easily add extra data (including display names) to a user's account via a FirebaseUser object.

We'll create a new method that will set the user's name:

CreateAccountActivity.java
...
private void createFirebaseUserProfile(final FirebaseUser user) {

        UserProfileChangeRequest addProfileName = new UserProfileChangeRequest.Builder()
                .setDisplayName(mName)
                .build();

        user.updateProfile(addProfileName)
                .addOnCompleteListener(new OnCompleteListener<Void>() {

                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        if (task.isSuccessful()) {
                            Log.d(TAG, user.getDisplayName());
                        }
                    }

                });
    }
...
  • To set the name, we first need to build a new UserProfileChangeRequest object. This is a Firebase object used to request updates to user profile information.

  • We call the setDisplayName() method to attach the user-entered name to the user's profile.

  • We then pass this UserProfileChangeRequest object into the updateProfile() method and attach an OnCompleteListener.

  • The OnCompleteListener will trigger the onComplete() method when the request is finished processing. If the request was successful, we log the name to the logcat.

We then need to call this method once our new FirebaseUser has successfully been created:

CreateAccountActivity.java
private void createNewUser() {
        ...

        mAuth.createUserWithEmailAndPassword(email, password)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {

                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {

                        mAuthProgressDialog.dismiss();

                        if (task.isSuccessful()) {
                            Log.d(TAG, "Authentication successful");
                            createFirebaseUserProfile(task.getResult().getUser());
                        } else {
                            ...
                        }
                    }
                });
         ...
  • To pass the user object to our createFirebaseUserProfile() method, we can grab the result from the Task object returned in onComplete(). We may then retrieve the specific user by calling Firebase's getUser() method.

Displaying Saved Names

Finally, let's update the MainActivity so that the user sees a welcome message in the AppBar when they have successfully logged in. We'll start by creating an AuthStateListener just like we did in our CreateAccountActivity, remembering to add it to our FirebaseAuth object in the onStart() method:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    private FirebaseAuth mAuth;
    private FirebaseAuth.AuthStateListener mAuthListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        mAuth = FirebaseAuth.getInstance();
        mAuthListener = new FirebaseAuth.AuthStateListener() {
            @Override
            public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
                 //display welcome message
            }
        };
        ...
    }

  @Override
    public void onStart() {
        super.onStart();
        mAuth.addAuthStateListener(mAuthListener);
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mAuthListener != null) {
            mAuth.removeAuthStateListener(mAuthListener);
        }
    }
...

Now we just need to display the welcome message:

MainActivity.java
...
mAuth = FirebaseAuth.getInstance();
        mAuthListener = new FirebaseAuth.AuthStateListener() {
            @Override
            public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
                FirebaseUser user = firebaseAuth.getCurrentUser();
                if (user != null) {
                    getSupportActionBar().setTitle("Welcome, " + user.getDisplayName() + "!");
                } else {

                }
            }
        };
...
  • We can use the getSupportActionBar() method to access our activity's action bar and then call the setTitle() method to update the text with our welcome message.

  • We do this in the onCompleted() method in our AuthStateListener because we need to have access to the FirebaseUser to grab the display name.

Clearing Old Accounts

Before we run our app, let's navigate to our Firebase app's Auth page and manually delete each user. We previously created users without attaching a display name. So, let's remove those old users to ensure that all users have a name to display. Otherwise, we might instead see null where a user's name should populate in our App Bar.

firebase-panel-auth

firebase-panel-auth

Let's run our app (making sure to log out any currently-authenticated users), navigate to CreateAccountActivity, and create a brand new user. After creating this new account, our app should navigate to MainActivity, and we should see this user's name in the AppBar:

username-displayed-appbar


Example GitHub Repo for MyRestaurants

Terminology


  • Progress Dialog: An animated progress indicator that is displayed along with a message. Often used to indicate that the application is still loading; and not frozen.

  • Android Patterns: Commonly-used regular expressions built right into Android.

Examples


Additional Resources