Material Design Android App Onboarding. A Tale From The Front Line | KISS digital

Lidia

When she was a little girl she pictured herself as a geologist or an oceanographer, but then, somehow, she pursued a master’s degree in Italian philology.

29 May 2017

Material Design Android App Onboarding. A Tale From The Front Line

postMainImage

It’s a good UX practice to show your users around when they first launch your app. While developing a mobile app, you can do it in various ways, depending on your case. We decided to opt for progressive user onboarding, as there was some not-so-obvious functionality that we wanted to make sure users get familiar with. If you’d want to wrap it in Material Design specs terms, we went for a user onboarding Quickstart composed of a few feature discovery elements.

Disclaimer: I am not claiming my solution for the problems below is perfect. This is how I was able to accomplish while developing a mobile app in question, I think there could be more elegant solutions. If you’d have something done differently or think my code does not make sense, please let me know.

A fight with Null Pointer Exceptions and state soup

The Goal

The app: it’s a mobile app, meant for the users of a website that serves as a tool for collecting various factoring data. When a user opens the app for the first time, the drawer is opened with a circular reveal prompting to tap Settings menu item. If the user taps the target, she/he is taken to Settings menu.

Upon returning to the main app screen, she/he is being presented with subsequent circular reveal animations (first one for FAB, the other for the center navigation button), which she/he can tap or dismiss by tapping outside the animation.


There are three more animations to show: when the user first opens the drawer by herself/himself:

]

and when the user first opens Contractors/Invoices screens:

All animations are shown only once.

Material Tap Target Prompt

Ok, so we know what we need to do, but how to do it? Surely there must be some other way than me carefully crafting out every single circular reveal animation. And there is! There are a few open source libraries that you can use, but Material Tap Target Prompt was spot-on for me. It complies with Material Design specs, supports minSdkVersion 7, is being actively maintained, and if you ask for additional features - there is a pretty good chance they’ll be added in a future release. With this library at hand, the problems I had were not with drawing the animations but managing their timing.

Problems

When I first started writing this article, I wanted to explain exactly how I deal with each user onboarding animation and what are the caveats in this particular case. But my post began to blow out of proportions, and I decided that it will be more reasonable to just break the whole thing down to 3 major problems encountered and explain how I dealt with them.

So, my main problems while developing this mobile app were… actually, THE problem was THE timing. But, if I had to break it down: detecting whether I should show the animations (first or subsequent uses of the app), showing them at the right time so that Android can draw them appropriately and preventing undesired user interaction during key onboarding moments.

SharedPreferences To The Rescue

It was essential to detect whether a user opens the app or sees its certain parts for the first time. Your Android friend for storing data of that kind is called SharedPreferences. Let’s warm up with the easy part: the onboardings that just need to show up when the user sees the respective screen for the first time and that are not dependent on any other onboardings. This is the code for animation that needs to show when the user first opens Invoices screen:

public void showOnboarding(int tabToSelect) {
      if (exampleCircularRevealPrompt != null) return;

       exampleTabLayout.post(() -> {
          //find your view here
          }
          exampleCircularRevealPrompt = new MaterialTapTargetPrompt.Builder(YourActivity.this)
                  .setTarget(view)
                  // set all the desired characteristics of your animation here using
                  MaterialTapTargetPrompt methods
                  .setOnHidePromptListener(new MaterialTapTargetPrompt.OnHidePromptListener(){
                      @Override
                      public void onHidePrompt(MotionEvent event, boolean tappedTarget) {
                          //to make the prompt disappear just null it
                          exampleCircularRevealPrompt = null;
                          // do whatever is necessary in the app after animation finishes here
                      }
                      @Override
                      public void onHidePromptComplete() {}
                  })
                  .create();
          exampleCircularRevealPrompt.show();
      });
  }

What exactly am I doing here? After returning (if my reveal animation isn’t null), inside a handler (I am getting an NPE without the post() method) I am finding my target - the element around which the reveal animation will be centered. In my case, it was a TextView on the first child of a tab layout, but it can be any View you like. Some, of course, will be less straightforward to ‘find’ that others, but that’s not the focus of my article.

Using MaterialTapTarget.Builder() I set all the animation parameters I need. I am not going to go into details here, if you require to use this library, you’ll find all the necessary info in the documentation. Then, before I call the method in the presenter, I need to do a simple check: is user seeing this screen for the first time? I need to call showOnboarding() inside presenter’s onCreate() method, more specifically in the Observable sequence’s onCompleted().

     () -> {
             if (view != null) {
                        view.initPager(allProducts);
                          if (!allProducts.isEmpty()) {
                            if (onboardingStatePrefs.isTargetFirstTimeSeen()) {
                              view.showOnboarding(0);
                                onboardingStatePrefs.setTargetOnboardShown();
                                }
                             }

Here come the SharedPreferences. You can notice the isTargetFirstTimeSeen() and setTargetOnboardShown() methods inside the code. If you’re familiar with SharedPreferences, you can pretty much anticipate what their implementations look like. Here I am just checking if a user sees the invoices screen for the first time and if so I am showing the onboarding animation and notifying the app via SharedPreferences that the animation has been shown. Here’s the SharedPrefs logic:

public boolean isTargetFirstTimeSeen() {
    return sharedPreferences.getBoolean(SHARED_PREFS_TARGET_ONBOARDING_SHOWN, true);
}

public void setTargetOnboardShown() {
    sharedPreferences
            .edit()
            .putBoolean(SHARED_PREFS_TARGET_ONBOARDING_SHOWN, false)
            .apply();
}

What I also did was creating a separate preferences class OnboardingStatePrefs just for the onboarding state logic for the whole app, as it turned out there was a lot of it and I didn’t want to clutter app’s main Configuration class.

The SharedPreferences part (first or subsequent time seeing the screen) is pretty much the same for all the onboarding animations.

onDrawerOpened(), post() and postDelayed() aka Please Don’t Draw Too Fast

The second serious problem I had was with drawing the animations at the appropriate time. If you don’t do it right, you can expect some really annoying glitches or lots of null pointer exceptions (especially when you attempt to draw on an element that has not yet been initialized).

I handled bad timing in most cases by using post() or postDelayed() methods. It’s not sophisticated, but it works if you need to stop Android from drawing things prematurely. Most don't mean all and yep, there was this one animation that would act rebellious, the worst being that it was THE VERY FIRST ONE that the users see when they open the app for THE VERY FIRST TIME. AKA the one to be drawn on the open drawer’s menu item. My best guess is that before the treatment it would start drawing while the drawer was opening and when the drawer finished opening it would re-draw, hence jumping up and down. Here’s the simple treatment it got:

@Override
public void showDrawerMenuItemOnboarding() {
       drawer.openDrawer(GravityCompat.START);
       setDrawerState(true);
       drawerWasOpen = true;
       if (drawerItemPrompt != null) return;

   drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
       @Override
       public void onDrawerOpened(View drawerView) {
           drawer.removeDrawerListener(this);

           if (navigationRecycler.findViewHolderForAdapterPosition(0) != null) {
            //find your target view here

               drawerItemPrompt = new MaterialTapTargetPrompt.Builder(ExampleActivity.this)
                       .setTarget(view)
                         // set all animation characteristics here using 
                         MaterialTapTargetPrompt methods
                       .setOnHidePromptListener
                       (new MaterialTapTargetPrompt.OnHidePromptListener() {
                           @Override
                           public void onHidePrompt(MotionEvent event, boolean tappedTarget) {
                               drawerItemPrompt = null;
                           }

                           @Override
                           public void onHidePromptComplete() {
                               drawer.addDrawerListener(drawerListener);
                               setDrawerState(false);
                           }
                       })
                       .create();
               drawerItemPrompt.show();
           }
       }
   });
}

I decided to use SimpleDrawerListener() interface here, for which you don’t have to implement all four abstract methods as opposed to DrawerListener(). The one I need is simply onDrawerOpened(). I just listen for the end of the opening process and as soon as it’s finished I draw the animation. Thanks to this Android draws my reveal circles when the state of the drawer is settled and I get rid of problems with my animation “jumping”.

Ok, do you see anything wrong with this logic?

I just realized that if it’s coded like this, when the user decides to dismiss app before the initial onboarding animation is ended and the drawer had the chance to close, the subsequent animations will never trigger. This is an unlikely scenario, but of course, we need to make sure it won’t happen. If you take a look at the code above, you’ll notice two more places that have to do with our pesky drawer. In the app, I really need to show two onboarding animations on the drawer in its ‘opened’ state:

I also need to show FAB onboarding animation as soon as drawer closes for the first time (either when user taps outside animation to dismiss it, or after he/she returns from the settings menu for the first time). I set the listener inside onHidePromptComplete(), which triggers after the first onboarding animation is finished. That’s obviously not all, I also need to tell it what to do when it detects drawer was closed for the first time - to trigger FAB and center navigation button onboardings - and opened by the user for the first time - to trigger share button onboarding. Here’s how it looks like in the code:

 private DrawerLayout.DrawerListener drawerListener = new DrawerLayout.SimpleDrawerListener(){
            @Override
            public void onDrawerOpened(View drawerView) {
                if (drawerOpened) {
                    closePrompt(centralNavButtonPrompt);
                    closePrompt(floatingActionButtonPrompt);
                    closePrompt(menuItemPrompt);
                    presenter.triggerOnboardingShareInfo();
                }
            }

            @Override
            public void onDrawerClosed(View drawerView) {
                presenter.triggerOnboardingFloatingActionButton();
            }
        };

Why all the closePrompt() calls? What they do is simply dismiss onboarding prompts to further make sure the animations will not be overlaying. drawerOpened() is a boolean value that you can set to true or false accordingly (notice the use in the previous chunk of code).

Blocking User Interaction With The Drawer

What’s the deal with the setDrawerState() method? Here’s how it looks like:

public void setDrawerState(boolean isEnabled) {
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this, drawer, getToolbar(), R.string.navigation_drawer_open,
                R.string.navigation_drawer_close) {};
        drawer.addDrawerListener(toggle);
        if (isEnabled) {
            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
            toggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_UNLOCKED);
            toggle.setDrawerIndicatorEnabled(true);
            toggle.syncState();
        } else {
            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
            toggle.onDrawerStateChanged(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
            toggle.setDrawerIndicatorEnabled(false);
            toggle.syncState();
        }
    }

Can you guess why do I have to use it? Well, who knows what weird things users will be trying to do with the drawer while in the process of onboarding, right? My setDrawerState() has one objective really: to prevent users from opening the drawer when it could mess up the process of showing appropriate onboarding animation. Mess up like this:

Thanks to setDrawerState(false) I can disable user interaction with the drawer whenever there’s a chance they will try to open it while the onboarding continues. This, combined with closePrompt() calls, prevents the possible overlaying of animations. Remember how I wrote about animations drawing to fast? Guess what, they can also draw too slow and it’s your job to make sure they will draw just when they need to be drawn.

Overview

In one of Jake Wharton’s presentations about RxJava I heard the term “state soup”. I cannot help but think that the term can be used to describe my code above. The most problematic element while developing this mobile app, was the drawer. There are many user onboarding events linked with it either opening or closing, for the first or subsequent times. I couldn’t think of any better structure for my code than relying on various DrawerListeners or blocking user interaction where I thought it could mess up onboarding animations.

I also learned that the timing is crucial when it comes to calling methods on elements that could have not been initialized yet (getting lots of NPEs) or drawing on elements whose state is not yet settled (the drawer again!), because you can produce some annoying glitches.

If you need to check if the user is opening your app or its particular screen for the first or subsequent times, then of course SharedPreferences will get the job done.

How do you normally approach user onboarding? Would you have done something differently when developing such a mobile app?

Lidia

When she was a little girl she pictured herself as a geologist or an oceanographer, but then, somehow, she pursued a master’s degree in Italian philology. She still loves languages, but you all know how awkward talking to people gets sometimes, so she decided to start talking to computers instead.