In the three preceding articles, I talked about the fly-in application menu and how to implement it on Android. For those who want to catch up on the Making of Prixing series, here are the links:
- The making of Prixing #1: Fly-in app menu
- The making of Prixing #2: Swiping the fly-in app menu
- The making of Prixing #3: Polishing the sliding app menu
This time, I would like to talk about the way we’ve managed notifications in Prixing 3.0. Indeed, Prixing for Android is a rather complex application containing tons of screens. Each of these screens can notify the user about something important: an error occurred while connecting to the server, you earned a new badge, the query you typed is too short, etc. By default, Android provides two great ways to notify the user without blocking: status bar notifications and Toast
s. The first is dedicated to notifications happening outside of the application while the latter is for in-app notifications. Simply put, a Toast
is a non intrusive and non touchable notification that is always displayed on top of your application’s screens.
From my point of view, this class is amazing. I really love it and that’s probably one of the reason why I am getting so frustrated when developing on iOS. Indeed, the platform made by Apple has no equivalent by default. Developers generally use a UIAlertView
(iOS AlertDialog
equivalent) which is extremely annoying to the user as it completely blocks the user flow as long as one of the “Cancel” or “OK” buttons hasn’t been pressed. As a consequence, iOS developers tend to notify the user via in-layout notifications. It is not consistent throughout iOS apps but the usability gain definitely worth it. Toast
makes unobstrusive notifications amazingly easy as you can create new instances from everywhere as long as you have a reference to a Context
(Service
, Activity
, BroadcastReceiver
’s onReceive
method, etc.).
Unfortunately, Toast is far from being perfect and I am not entirely satisfied with it. Toast can be un-accurate in some cases. Indeed, Toast
has one major drawback: it completely breaks contexts. This issue can be reproduced effortless. Let’s say a user is currently in an app firing a Toast
and wants to switch to another application using the dedicated “multitask” button. The Toast will remain on screen even if the brought-to-front application has nothing do to with the previously shown app as described on the following figure:
Sample of a user flow in which Toast fails
As you can easily notice, the problem with Toast
s is they are persistent1. Once a Toast has been fired, it is displayed on top of any screen and remains visible for the duration specified at its creation.
Information is always context-related
When designing and developing an app, you always have to spend a lot of time thinking about contexts. Contexts are insanely important as they define the space/time frame an information can live in. Exposing information in the wrong context will make your UI incomprehensive and will radically degrade your users' experience. Can you imagine Google announcing a new Android version in the middle of the annual world dental congress? I guess and hope your anwser is no. This example clearly demonstrates information and context are close and depend on each other.
As of this logic, Prixing currently features 3 main contexts:
- The application context is where images and network data live in. All of this information is not tied to a particular screen or widget and are shared among all Prixing’s screens.
- The Activity context contains the information belonging to a screen (profile, nearby stores, products list, etc.).
- The application menu context: Introduced by the fly-in app menu, the application menu context describes the entire app navigation. It is completely independent from the two previous contexts.2
In order to bypass the Toast
persistence problem and ensure information is displayed in the correct context, we decided to create a new notification system in Prixing: Activity
-bound notifications. In reference to the Toast
class and to my French culture I decided to name it a Crouton
(if you don’t know what a Crouton is go to this Wikipedia page). This is what it looks like in the current version of Prixing:
Crouton
overcomes the main issue of having a Toast
being shown while the menu is open. It sticks to the current screen sliding with it and leaving the menu completely free of any information that would have not been related to it.
Introducing the Crouton API
The Android framework lets you interact with the Toast
class via a very simple API. You usually only need a single line of code to create and show a notification. We wanted Crouton
to be at least as easy to use as the Toast
class so we engineered an insanely simple API:
- Public Constants
int STYLE_ALERT
: Show the text notification for a long period of time with a negative style.int STYLE_CONFIRM
: Show the text notification for a short period of time with a positive style.int STYLE_INFO
: Show the text notification for a short period of time with a neutral style.
- Public Methods
static Crouton makeText(PrixingActivity prixingActivity, CharSequence text, int style)
: Make a Crouton with the given text and style.static Crouton makeText(PrixingActivity prixingActivity, int resId, int style)
: Make a Crouton with the given text (extracted from the resource identifier) and style.void show()
: Show the view with the specified style.
Yes, you’re right! No visible constructors, just 2 static factory methods, a public method and 3 constants. The two main differences with Toast
are you always need a PrixingActivity
instead of a Context
and the third parameter isn’t the duration but the style of the notification.
Contrary to the Toast
class, Crouton
automatically computes the duration of the notification depending on the style. Pretty logically, we decided an alert notification had to be visible for a larger amount of time than a confirmation or neutral information. People usually spend more time on understanding an issue than a validation message. As a result, we decided to use 3000 milliseconds instead of 1300 milliseconds.
Crouton
handles multiple notifications fired at the same time just like Toast
does. In order to do so, it stacks Crouton
s in a queue if another instance is currently being displayed. This makes sure no notification is lost as long as the user remains on the emitting Activity
Implementing in-layout notifications
The Android framework contains several classes inheriting from Activity
. Each of this Activity
-subclasses can be used to manage particular components. For instance, ListActivity
manages a ListView
, FragmentActivity
manages Fragment
s and Loader
s, MapActivity
manages MapView
, etc. All of these subclasses may look helpful for developers but they can also be a pain in the ass when engineering a rather large project. Java not allowing multiple inheritance and not supporting categories/extension methods (yet?) makes it pretty boring and tedious to add features to Activity-subclasses. As a direct consequence, trying to have a consistent layout in your Activity is hard. So here is a tiny advice to you: always favor composition over inheritance in your projects.
To overcome this issue, all Prixing Activity
implements an interface called PrixingActivity
. Roughly speaking, PrixingActivity
manages several Activity
-based behaviors and ensures the Activity root layout is as follows:
As described on the previous figure, a PrixingActivity
always contains a TextView
which is laid out by our custom TitleBarHost
in a way it is above the TitleBar
(custom implementation of the ActionBar
) and slides down when the notification needs to be shown.
To have consistent and smooth animations, we used the same interpolator introduced in the first part of this serie when showing/hiding a Crouton
: the SmoothInterpolator
.
Finally, some of you may wonder why the notification is not lazily created/inflated thanks to a ViewStub
. Crouton
currently relies on a single TextView
. Using a ViewStub
is clearly not a performance boost in this case as the final View
hierarchy is small. Keep in mind a ViewStub
remains a View
. As a result, using a ViewStub
would have resulted in inflating 1-min or 2-max View
. The method used in Prixing ensures only a single TextView
is inflated. If you really care about not creating a TextView
that is often never used, you can lazy-create it the first time it is requested.
In-layout notifications may not be perfect but they can solve lots of UI/UX problems. Implementing such a mechanism in Prixing made the application easier to use and more logical. The fly-in app menu clearly outlined some of the major drawbacks of the Toast
, Crouton
solved. Don’t use Toast
blindly, think about your application’s context and the information you need to display. In other words: leave the engineering mindset aside and start thinking like a real user.
Thanks to @franklinharper for reading drafts of this.
A Toast is actually theoritacally persistent if and only if you don’t call
Toast#cancel()
. In practice, nobody keeps a reference on the firedToast
, theToast#cancel()
documentation clearly states it shouldn’t be used and, last but not least, the currentToast#cancel()
implementation contains a magnificent// TODO
annotation informing the method is not complete:// TODO this still needs to cancel the inflight notification if any
Contexts are everything! This is I why I believe an
ActionBar
has to slide with the content when using a fly-in app menu. AnActionBar
should only be used to describe the current screen/Activity
context. As a result it has nothing to do with the application menu. The current version of Prixing, Google+, Spotify, etc. have it right while some others such as Youtube have it wrong in my opinion.