Lecture 8 Notifications & Settings
This lecture discusses some additional user interface components common to Android applications: Notifications, Settings Menus. and Dialogs 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/lecture08-notifications-settings.
8.1 Dialogs
We have previously provided feedback to users via simple pop-ups such as Toasts or Snackbars. However, sometimes you would like to show a more complex “pop-up” View—perhaps one that requires additional interaction.
A Dialog28 is a “pop-up” modal (a view which doesn’t fill the screen) that either asks the user to make a decision or provides some additional information. At it’s most basic, Dialogs are similar to the window.alert()
function and its variants used in JavaScript.
There is a base Dialog
class, but almost always we use a pre-defined subclass instead (similar to how we’ve use AppCompatActivity
). AlertDialog
29 is the most common version: a simple message with buttons you can respond with (confirm, cancel, etc).
We don’t actually instantiate an AlertDialog
directly (in fact, its constructors are protected
so inaccessible to us). Instead we use a helper factory class called an AlertDialog.Builder
. There are a number of steps to use a builder to create a Dialog:
Instantiate a new builder for this particular dialog. The constructor takes in a
Context
under which to create the Dialog. Note that once the builder is initialized, you can create and recreate the same dialog with a single method call—that’s the benefits of using a factory.Call “setter” methods on the builder in order to specify the title, message, etc. for the dialog that will appear. This can be hard-coded text or a reference to an XML String resource (as a user-facing String, the later is more appropriate for published applications). Each setter method will return a reference to the builder, making it easy to chain them.
Use appropriate setter methods to specify callbacks (via a
DialogInterface.OnClickListener
) for individual buttons. Note that the “positive” button normally has the text"OK"
, but this can be customized.Finally, actually instantiate the
AlertDialog
with thebuilder.create()
method, using theshow()
method to make the dialog appear on the screen!
//java
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Alert!")
.setMessage("Danger Will Robinson!");
builder.setPositiveButton("I see it!", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// User clicked OK button
}
});
AlertDialog dialog = builder.create();
dialog.show();
//kotlin
val builder = AlertDialog.Builder(this)
builder.apply {
setTitle("Alert!")
setMessage("Danger Will Robinson!")
setPositiveButton("I see it!") { dialog, id ->
Log.v(TAG, "You clicked okay! Good times :)")
}
}
An important part of learning to develop Android applications is being able to read the API to discover effective options. For example, can you read the AlertDialog.Builder API and determine how to add a “cancel” button to the alert?
While AlertDialog
is the most common Dialog, Android supports other subclasses as well. For example, DatePickerDialog
and TimePickerDialog
provide pre-defined user interfaces for picking a date or a time respectively. See the Pickers guide for details about how to utilize these.
DialogFragments
The process described above will create and show a Dialog, but that dialog has a few problems in how it interacts with the rest of the Android framework—namely with the lifecycle of the Activity in which it is embedded.
For example, if the device changes configurations (e.g., is rotated from portrait to landscape) then the Activity is destroyed and re-created (it’s onCreate()
method will be called again). But if this happens while a Dialog is being shown, then a android.view.WindowLeaked
error will occur and the Dialog is lost!
To avoid these problems, we need to have a way of giving that Dialog its own lifecycle which can interact with the the Activity’s lifecycle… sort of like making it a modular piece of an Activity… that’s right, we need to make it a Fragment! Specifically, we will use a subclass of Fragment called DialogFragment
, which is a Fragment that displays as a modal dialog floating above the Activity (no extra work needed).
Just like with the previous Fragment examples, we’ll need to create our own subclass of DialogFragment
. It’s often easiest to make this a nested class if the Dialog won’t be doing a lot of work (e.g., shows a simple confirmation).
Rather than specifying a Fragment layout through onCreateView()
, we can instead override the onCreateDialog()
callback to specify a Dialog
object that will provide the view hierarchy for the Fragment. This Dialog can be created with the AlertDialog.Builder
class as before!
//java
public static class HelloDialogFragment extends DialogFragment {
public static HelloDialogFragment newInstance() {
Bundle args = new Bundle();
HelloDialogFragment fragment = new HelloDialogFragment();
fragment.setArguments(args);
return fragment;
}
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
//...
AlertDialog dialog = builder.create();
return dialog;
}
}
//kotlin
class HelloDialogFragment : DialogFragment() {
companion object {
fun newInstance(): HelloDialogFragment {
val args = Bundle()
val fragment = HelloDialogFragment()
fragment.arguments = args
return fragment
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { //confirm Activity isn't null
val builder = AlertDialog.Builder(it)
builder.apply {
//...
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
Finally, we can actually show this DialogFragment
by instantiating it (remember to use a newInstance()
factory method!) and then calling the show()
method on it to make it show as a Dialog. The show()
method takes in a FragmentManager
used to manage this transaction. By using a DialogFragment, it is possible to change the device configuration (rotate the phone) and the Dialog is retained.
Here’s the other neat trick: a DialogFragment
is just a Fragment
. That means we can use it anywhere we normally used Fragments… including embedding them into layouts! For example if you made the DetailsFragment
subclass DialogFragment
instead of Fragment
, it would be able to be used in the exact same as before. It’s still a Fragment, just with extra features—one of which is a show()
method that will show it as a Dialog!
- Use
setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog)
to make the Fragment look a little more like a dialog.
The truth is that Dialogs are not very commonly used in Android (compare to other UI systems). Apps are more likely to just dynamically change the Fragment or Activity being shown, rather than interrupt the user flow by creating a pop-up modal. And 80% of the Dialogs that are used are AlertDialogs. Nevertheless, it is worth being familiar with this process and the patterns it draws upon!
8.2 Notifications
We can let the user know what is going on with the app by popping up a Toast or Dialog, 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 Notifications30. 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 follow through the documentation to add a feature that when we click on the “notify” button, a notification will appear that reports how many times we’ve clicked that button.
To follow along this, open up the Notifications documentation at https://developer.android.com/training/notify-user/build-notification.
Looking at the documentation we see a link to the Notifications Overview to start. That page also links to the Notification Design Guide, which is a good place to go to figure out how to design effective notifications.
I personally prefer to work off of sample code, modifying it until I have something that does what I want. So I suggest scrolling down slowly until you find an example you can copy/paste in, or at least reference. Then you can scroll back up later to get more detail about how that code works. The table of contents is also useful for this.
Eventually you’ll find a subsection “Create a basic notification”, which sounds like a great place to start!
The first part of creating this Notification is using NotificationCompat.Builder
(use the v4
support version; the v7
is now deprecated). We saw 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 properties of the Notification.
Starting in API 26 (Oreo), we also are required to specify a notification channel (called a “notification category” in the user interface). These are used to help users manage notifications by grouping them together and allowing users to control the settings of that group—such as whether they are shown, what sounds they play, etc.
Notification channels are identified by a String ID that is unique within the package, so we’ll usually define this as a constant. We can make a single channel for our whole app for now. We’ll actually set up the channel later, but all notifications need to be associated with one.
- Note that you need to do this if you target API 26 or later; otherwise the method signatures change a bit.
Going through those settings: I don’t have a drawable resource to use for an icon, which makes me want to not include the icon specification. Looking up a bit you’ll notice that notification icon is required, so we will need to make one.
We can produce an new Image Asset for the notification icon (
File > New > Image Asset
), just as we did previously with launcher icons and Material icons. Specify the “type” asNotification
, give it an appropriate name, and pick a clipart of your choosing.We set the “title” and the content for the notification.
The “priority” is how important the notification is (e.g., whether it should interrupt the user or not). It’s used for API 25 and lower; API 26 (Oreo) builds this into the Notification Channel’s “importance” instead. But we set it here for backwards compatibility.
The next step is to actually construct the Notification Channel. While we’ve given it a name, we need to actually make the object to manage that channel’s settings.
Note that the code to run channels is only available for API 26 (Oreo). Since they’re not in the support library, we have to use an
if
statement to make sure we don’t try to run the code on older devices! We do this by comparing against theBuild.VERSION.SDK_INT
:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Create the NotificationChannel }
The channel is instantiated as a new
NotificationChannel
object, and is passed an ID (so it knows what it’s name is), a display label, and an importance.For importance,
IMPORTANCE_HIGH
will cause it to pop up and make a sound. See the documentation for other options. Also check out the design guide for best practices on specifying Notification priority.For earlier versions of Android, you set the
priority
. In order for a notification to visually intrude upon the user, its priority must be high enough (it needs to beNotificationCompat.PRIORITY_HIGH
or higher) and because it needs to use either sound or vibration (it needs to be really important to get a heads-up pop).You 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. The pattern is[delay, vibrate, sleep, vibrate, sleep, ...]
You can also assign a default sound with (e.g.)builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
We use the
NotificationManager
(not the support library one!) to “register” the channel, so that notifications can be added to it.val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel)
Skipping ahead, we can actually show the notification that we’ve created so far. We 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. This will allow us to refer to and update that Notification later. We can update this notification later by just issuing a Notification with the sameID
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!
8.2.1 Tap Actions
We’ve managed to show notifications, but we also want them to do something, such as when you click on it—specifying the Notification Action. At the very least, clicking on the notification should open up the relevant application. And since Intents
are messages to open Activities, it makes sense that clicking a Notification would send an Intent
. Moreover, remember that notifications exist outside of the app that creates them; so we need to use Intents to send messages (we can’t just specify a callback function).
But beyond just making the Intent
to send, we’re actually going to create what is called a PendingIntent
. The details are not super readable, but it’s basically a wrapper around an Intent
that we give to another class. Then when that other component wants to send the Intent
, it can “unwrap” it from the PendingIntent
and run the command as if it were us (in terms of the “source”, permissions, etc). The Intent is one that we sent, but is “pending” delivery/activation by another service. So when we select the notification, the Notification Drawer service can open up that PendingIntent
packet, pull out the Intent
we want to send, and then mail it off. And this Intent that the Notification Drawer sends is to wake up our Activity, and it is sent with our permissions (rather than the Drawer’s permissions), as if we had sent it ourselves!
This is like when you ask a professor for a letter of recommendation, and you give them the stamped envelope (which you should always do!). When they get the packet, then can then send their letter using your envelop and stamp!
We create a
PendingIntent
using thePendingIntent.getActivity()
method—this takes in a Context, an ID code (similar to the request codes we use when sending Intents for Results; this allows us to know whichPendingIntent
a command came from), theIntent
to be launched, and a flagPendingIntent.FLAG_CURRENT_UPDATE
so that if we re-issue the PendingIntent it update the existing one instead of replacing it with a new pending action (e.g., for if we update our notification’s action).Note that you can create an “artificial backstack” (e.g., if the Notification is supposed to drop the user into the middle of your app’s workflow) by using the
TaskStackBuilder
class. This builder will let you specify a “history” of intents, and use that to produce the properPendingIntent
.Be sure and tell the
NotificationCompat.Builder
about the content intent we just defined!
Now we can click on the Notification and have it do something! Note that, as always, there are a number of other pieces/details/features we can specify, but I leave those to you to look up in the documentation.
As the course focuses on development, this lecture references but does not discuss the UI Design guidelines. For example: 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. Major HCI and Mobile Design guidelines apply here as well (e.g., make actions obvious, give feedback, avoid irreversible actions, etc.).
8.3 Settings
The last 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
.
Preference Settings
While a SharedPreferences file 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.
- Yes, the term “preferences” can be used to mean either “shared preferences” or “user preferences” (or both if they are the same thing!) depending on the context.
A “User 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
32 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 a<PreferenceScreen>
(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 Settings screen). 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 use when storing the preference in the SharedPreferences fileandroid:title
a user-visible name for the Preferenceandroid: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! Since the Settings will be their own screen, we’ll need to make another Activity (though it won’t need a layout!). To actually render the Preferences XML, we’ll use the PreferenceFragment
class (a specialized Fragment for showing lists of Preference
objects); it’s usually easies to create this as a nested class in the Activity.
For the Fragment, 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!For the Activity, we just need to have it load 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 the container for our Fragment is
android.R.id.content
. This 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! In effect, the PreferenceFragment
automatically edits the SharedPreferences based on the user interaction. We never need to write to the file ourselves, just read from it (and we read from it in the exact same way we read any other SharedPreferences file, as described above).
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.
https://developer.android.com/reference/android/support/v7/app/AlertDialog.html↩
https://developer.android.com/guide/topics/ui/notifiers/notifications.html↩
https://developer.android.com/guide/topics/data/data-storage.html#pref↩
https://developer.android.com/reference/android/preference/Preference.html↩