Lecture 9 UI Components II
This lecture discusses how to include additional user interface components in an Android application: namely Notifications31 and Settings Menus. As before, this lecture aims to provide exposure rather than complete coverage to these concepts; for more options and examples, see the official Android documentation.
This lecture references code found at https://github.com/info448-s17/lecture09-notifications-settings.
9.1 Notifications
We have previously covered how to let the user know what’s going on by popping up a toast or even an AlertDialog
, but often we want to notify the user of something outside of the normal Activity UI (e.g., when the app isn’t running, or without getting in the way of other interactions). To do this, we can use Notifications. These are specialized views that show up in the notification area (the icons at the top of the operating system display) and in the system’s notification drawer, which the user can get to at any point—even when outside the app—by swiping down on the screen.
Android’s documentation for UI components is overall quite thorough and usable (after all, Google wants to make sure that developers can build effective apps, thereby making the platform worthwhile). And because there are so many different UI elements and they change all the time, in order to do real-world Android development you need to be able to read, synthesize, and apply this documentation. As such, this lecture will demonstrate how to utilize that documentation and apply it to create notifications. We will utilize this documentation in order to add a feature that when we click on the appropriate menu button, a notification will appear that reports how many times we’ve selected that option.
To follow along this, open up the Notifications documentation.
Looking at the documentation we see an overview to start. There is also a link to the Notification Design Guide, which is a good place to go to figure out how to design effective notifications.
There is a lot of text about how to make a Notification… I personally prefer to work off of sample code, modifying it until I have something that does what I want, so I’m going to scroll down slowly until I find an example I can copy/paste in, or at least reference. Then we can scroll back up later to get more detail about how that code works.
Eventually you’ll find a subsection “Creating a Simple Notification”, which souds like a great place to start!
The first part of this Notification is using NotificationCompat.Builder
(use the v4
support version). We have previously seen this kind of Builder class with AlertBuilder
, and the same concept applies here: it is a class used to construct the Notification for us. We call setters to specify the properteis of the Notification
I don’t have a drawable resource to use for an icon, which makes me want to not include the icon specification. However, scrolling back up will reveal that a notification icon is required, so we will need to make one.
We can produce an new Image Asset for the notificaton icon (
File > New > Image Asset
), just as we did previously with launcher icons. Specify the “type” asNotification
, give it an appropriate name, and pick a clipart of your choosing.
The next line makes an Intent. We’ve done that too… but why create an Intent for a Notificatoin? If we scroll up and look where Intent
is referenced, we can find out about Notification Actions, which specify what happens when the user clicks on the Notification. Usually this opens the relevant application, and since Intents
are messages to open Activities, it makes sense that clicking a Notification would send an Intent
.
Notice that the
Intent
will actually be wrapped in aPendingIntent
. Thus we will give the Notification a PendingIntent, which contains an “RSVP” (to open the Activity) that it can send to the system when someone clicks on it. (the Intent is “pending” delivery/activation by another service).In particular, we use a PendingIntent in order to make sure that the Activity who will be executing it (the “notification service” component) will have permission to send the contained Intent. The Intent the notification service sends is to wake up our Activity, run with our permissions. It is as if we had sent the Intent ourselves!
The example Notification is also using the a TaskStackBuilder
to construct an “artificial” backstack. This is used to specify, for example, that if the user clicks on the Notification and jumps to a “detail” view (say), they can still hit the back button to return to the “master” view, as if they had navigated to the “detail” view following a normal application phone.
- We build this backstack not just with methods, but by integrating with the “parent-child” relationship we’ve otherwise set up between Activities. In the
Manifest
, we had specified thatSecondActivity's
parent isMainActivity
. This is what gave us the nice back button in the ActionBar. These sequence ofparentActivityName
attributes form a hierarchy that will be the “back navigation hierarchy.” We add the “endpoint” of the hierarchy to the builder usingaddParentStack(MyResultActivity.class)
, and then finally put theIntent
we actually want to use “on top” of the stack withaddNextIntent(resultIntent)
.
The resultIntent
is not the PendingIntent… yet. While we could define a PendingIntent manually, the example uses the TaskStackBuilder#getPendingIntent()
method to build an appropriate PendingIntent
object.
- Pass it an ID to refer to that request (like we’ve done when sending Intents for Results), and a flag
PendingIntent.FLAG_CURRENT_UPDATE
so that if we re-issue the PendingIntent it update instead of replace the pending Intent. - We can then assign that
PendingIntent
to the Notification builder (withsetContentIntent()
).
Finally, we can use the NotificationManager
(similar to the FragmentManager
, SmsManager
, etc.) to fetch the notification service (the “application” that handles all the notifications for the OS). We tell this manager to actually issue the built Notification
object.
- We also pass the
notify()
method anID
number to refer to the particular Notification (not the PendingIntent, but the Notification). Again, this will allow us to refer to and update that Notification.
This allows us to have working Notifications! We can click the button to launch a Notification, and then click on the Notification to be taken to our app, which has a working back stack!
We can also update this notification later, and it’s really straightforward: we simply re-issue a Notification with the same ID
number, and it will “replace” the previous one!
- For example, we can have our text be based on some instance variable, and have the Notification track the number of clicks!
You may notice that this notification doesn’t “pop up” in a way we might expect. This is because its priority isn’t high enough (it needs to be NotificationCompat.PRIORITY_HIGH
or higher) and because it doesn’t use either sound or vibration (it needs to be really important to get a heads-up pop).
- We can make the Notification vibrate by using the
setVibrate()
method, passing it an array of times (in milliseconds) at which to turn vibration on and off. - Pattern is
[delay, vibrate, sleep, vibrate, sleep, ...]
- We can also assign a default sound with (e.g.,)
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
- See the design guide for best practices on priority.
As always, there are a number of other pieces/details we can specify, but I leave those to you to look up in the documentation.
As a focus on development, this lecture references but does not discuss the UI Design guidelines: e.g., what kind of text should you put in your Notification? When should you choose to use a notification? Android has lots of guidance on these questions in their “design” documentation, and further HCI and Mobile Design guidelines apply here just as well. In general, this course will leave the UI design up to you. But major guidelines apply (e.g., make actions obvious, give feedback, avoid irreversible actions, etc.).
9.2 Settings
The second topic of this lecture is to support letting the user decide whether clicking the button should create notifications or not. For example, maybe sometimes the user just want to see Toasts! The cleanest way to support this kind of user preference is to create some Settings using Preferences
.
9.2.2 Preference Settings
While SharedPreferences acts a generic data store, it is called Shared Preferences because it’s most commonly used for “user preferences”—e.g., the “Settings” for an app.
The “Preference Menu” is a user-facing element, so we’ll want to define it as an XML resource. But we’re not going to try and create our own layout and interaction: instead we’re just going to define the list of Preferences
33 themselves as a resource!
- We can create a new resource using Android Studio’s New Resource wizard. The “type” for this is actually just
XML
(generic), though our “root element” will be aPreferenceScreen
(thanks intelligent defaults!). By convention, the preferences resource is namedpreferences.xml
Inside the PreferenceScreen
, we add more elements: one to represent each preference we want to let the user adjust (or each “line” of the screen Settings window). We can define different types of Preference
objects, such as <CheckBoxPreference>
, <EditTextPreference>
, <SwitchPreference>
, or <ListPreference>
(for a dialog of radio buttons). There are a couple of other options as well; see the Preference
base class.
These elements should include the following XML attributes (among others):
android:key
the key to store the preference in the SharedPreferences fileandroid:title
a user-visible nameandroid:defaultvalue
a default value for the preference (usetrue
orfalse
for checkboxes).- More options cam be found in the the
Preference
documentation.
We can further divide these Preferences to organize them: we can place them inside a
PreferenceCategory
tag (with its owntitle
andkey
) in order to group them together.Finally we can specify that our Preferences have multiple screens by nesting
PreferenceScreen
elements. This produces “subscreens” (like submenus): when we click on the item it will take us to the next screen.
Note that a cleaner (but more labor-intensive) way to do this if you have lots of settings is to use preference-headers
which allows for better multi-pane layouts… but since we’re not making any apps with that many settings this process is left as exercise for the reader.
Once we have the Preferences all defined in XML: we just need to show them in our application! To do this, we’re going to use the PreferenceFragment
class (a specialized Fragment for showing lists of Preference
objects).
- We don’t need to specify an
onCreateView()
method, instead we’re just going to load thatPreference
resource in theonCreate()
method usingaddPreferencesFromResource(R.xml.preferences)
. This will cause thePreferenceFragment
to create the appropriate layout!
We’ll put this Fragment inside a plain Activity
, which just loads that Fragment via a FragmentTransaction:
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
The Activity doesn’t even need to load a layout: just specify a transaction! But if we want to include other stuff (e.g., an ActionBar), we’d need to structure the Activity and its layout in more detail.
Note that
android.R.id.content
refers to the “root element” of the current View–basically whatsetContentView()
is normally inflating into.There is a
PreferenceActivity
class as well, but the official recommendation is do not use it. Many of its methods are deprecated, and since we’re using Fragments via the support library, we should stick with the Fragment process.
Finally, how do we interact with these settings? Here’s the trick: a preferences
XML resource is automatically associated with a SharedPreferences
file. And in fact, every time we adjust a setting in the PreferenceFragment
, the values in that file are edited as well! We never need to write to the file, just read from it (similar to any other SharedPreferences file).
The preference
XML corresponds to the “default” SharedPreferences
file, which we’ll access via:
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
- And then we have this object we can fetch data from with
getString()
,getBoolean()
, etc.
This will allow us to check the preferences before we show a notification!
That’s the basics of using Settings. For more details see the documentation, as well as the design guide for best practices on how to organize your Settings.