lilt: Lightweight Interactive Learning Tool

Topic 10

Dialogs and Notifications

In this topic we will:

  • Look at how to create dialog boxes in Compose
  • Look at notifications
  • Adding click handlers to Ramani Maps objects

Dialogs

When developing software we frequently have to catch the user's attention, by displaying a prominent message to the user or prompting the user to enter essential information. We do this by means of dialog boxes. Dialog boxes are pop-up boxes which appear in front of the main UI and either contain informatioin or prompt the user to enter information. In Compose, there are two main composables available to allow us to use dialogs: the AlertDialog and the plain Dialog.

Which of these you use depends on the requirements of your app. A very common type of dialog is one which presents important information to the user or asks the user to confirm an action. This will always contain some text, a "Dismiss" button (to hide the dialog), and a "Confirm" button (which allows the user to confirm an action). This type of dialog is known as an alert dialog, and because it is such a common type of dialog, there is a composable specialised for it - the AlertDialog.

If you wish to develop a more complex dialog, for example to allow the user to enter text or to pick an image or a colour scheme, then you should use the ordinary Dialog class together with a composable containing the content, for example a Surface or a Column.

We will look at each of these now.

AlertDialog

The AlertDialog is the simplest to code, as it does not require a custom layout. You specify common properties of the dialog, and Compose will build a dialog for you. Here is a simple example:

Note how we define the dialog with AlertDialog: it is placed within our material theme (so the theme will apply to it) but outside our main Column. Note how the AlertDialog takes various arguments:

  • icon (optional) - an icon which will appear at the top of the dialog.
  • title - the dialog title, appears at the top of the dialog, below the icon.
  • text - the main content of the dialog.
  • onDismissRequest - a callback function which runs when the user dismisses the dialog by clicking outside it.
  • dismissButton - the button to use for the dismiss action. Should have an onClick and some content, like a normal Button.
  • confirmButton - the button to use for the confirm action. Should have an onClick and some content, like a normal Button.

Note that we use state variables to control whether the dialog is visible or not. In our main column we have a "Book Tickets" button, which when clicked, sets the state variable dialogVisible to true. This triggers a recompose, so that when the composable is next drawn, the AlertDialog will be shown, as it's only displayed if dialogVisible is true.

Also note how if the user presses either the confirm or the dismiss buttons, the dialogVisible state variable is set to false, so the dialog will disapper. Additionally, if the user presses the confirm button, the dialogSubmitted state variable is set to true, which means that a confirmation message is rendered in the column rather than the original book button.

Using the standard Dialog

The AlertDialog is very useful if we want to produce a standard alert dialog with Confirm and Dismiss buttons. However if we want to implement a custom UI for the dialog, we should use the Dialog composable instead. With Dialog, you define a standard composable (such as a Surface or Column) containing your content, and wrap a Dialog round it to turn it into a dialog.

Here is an example showing a login dialog:

Note how we create a Dialog containing a Column as its content. This column implements a login form (note the visualTransformation of PasswordVisualTransformation to hide the password). When the button is clicked, we set the loginDialogVisible state variable to hide the dialog, and we also set a loggedIn state variable to true if the login details were correct. This could be used, for example, to show hidden content.

Notifications

Frequently we wish to inform the user of an event, such as receiving an email, or getting a new message in social media. Such a message is known as a notification. In Android, a notification is a message which appears as an icon at the top of the device and also appears in a notification list. The Notification class represents a notification.

Setting up permissions

To use notifications in an application targeting API level 33 (Android 13) upwards, you need to add the POST_NOTIFICATIONS permission to the manifest:

You should also perform runtime permission checking for POST_NOTIFICATIONS as by default, notifications for a given app may be turned off. The example below sets both ACCESS_FINE_LOCATION (for GPS) and POST_NOTIFICATIONS, and shows how you can request more than one permission at a time:

Note how we place our permissions in an array and then use the any() method of the array to check if any are not granted yet. any() takes a lambda containing a condition (here, has the given permission been granted?). If this condition evaluates to true for any member of the array, any() will return true. So in this case, any() will return true if at least one permission is not granted, and in this case, the permissions will be requested.

Additionally the minSdk should be at least 26 if you wish to use notification channels (see below).

Notification channels

On Android Oreo (API level 26) and upwards, notifications must be associated with a particular channel. Channels group together related notifications; all notifications on a given channel can be associated with the same sound or light colour (e.g. flashing green for text message, blue for a social media update, and so on. A user can allow or block all notifications on a particular channel for a particular app, by going to the settings for that app, selecting "Notifications", and turning that specific channel on or off.

To use channels, create a NotificationChannel object with a given ID.

Notification channels - example

The example was originally based on that provided here, but has been modified. More information on channels can be found on this page, including associating a channel with a notification light colour or vibration, organising channels into groups, opening the user's notification settings and deleting channels.

Note that:

  • nMgr is an object of type NotificationManager. It is a system-wide object used for managing notifications.
  • the channelID is a unique string identifier for that channel.
  • When we create a NotificationChannel, we supply the channel ID, the visible name of the channel ("Email notifications" here), and the channel importance. The importance controls how prominently the notifications are displayed.

Creating and Displaying a Notification

  • A notification builder is used to construct the Notification object, by specifying options for the notification such as the icon and the associated text. For example:

    This will create a notification on the channel channelID (see above) with the title "Time update" and full message text showing the current time in milliseconds since Jan 1 1970.

  • To actually show the notification, you should obtain the NotificationManager (we did this above when creating a notification channel) and call its notify() method, passing in the Notificiation object:

Making something happen when the user selects the notification

In many cases, we will want something to happen when the user clicks on a notification. or example, imagine a mapping app in which you receive a notification when you are nearby a point of interest. You might want a separate activity to launch when the user clicks on the notification, which displays full details of the point of interest (e.g a description, and reviews, for a pub or hotel).

To do this we need to use pending intents. Before looking at pending intents, we also need to look at Android's Intent class. An Intent is a representation of an action to launch, or communicate with, some other app component. For example, if we wish to create a second Activity (with Compose, less common than it used to be), we would create an Intent to launch the second activity. Intents can contain data, known as extras, which are useful if we wish to pass data between activities. These are stored in a collection of key/value pairs known as a Bundle. Intents also have an action: a string describing what the Intent does. Activities can receive many different Intents and we use the action to work out which Intent the activity has received, and act accordingly.

A pending intent is an Intent which will occur at some future time (e.g. when the user clicks on the notification) hence the name _Pending_Intent.

Here is an example of creating a pending intent which runs a separate activity, EmailActivity. Note we must first create an Intent, and then wrap it with a PendingIntent:

Note how we add the email message ID as an extra so that the recipient of the Intent can process it appropriately.

IMPORTANT UPDATE: Intent.FLAG_IMMUTABLE must be specified if targetSdk is 31 or more.

You then use the PendingIntent in a notification:

One common use-case for pending intents is to navigate back to the activity which generated the notification, by clicking on a notification. The main activity of an email app might notify you when an email has been received. However, you might be using another app at the time, with the email app in a paused state in the background. What should happen is that the email app should become visible again when you click on the notification.

This is a little more complex than you might think: you have to account for whether the email activity is already running in the background or not; if it is, it should become visible, but if it is not, it should be launched. The code above will achieve this. The key thing to understand here is that when activities are launched, they are placed on top of a stack of activities.

The key things to note here are the flags of the intent (note: setFlags() in Java). To repeat this code:

These mean the following:

  • FLAG_ACTIVITY_CLEAR_TOP - if there are other activities above the activity generating the notification, clear them from the activity stack
  • FLAG_ACTIVITY_SINGLE_TOP - if the activity is already the top activity on the stack (i.e. the one currently showing), do not relaunch it but use the existing copy of the activity
  • Without FLAG_ACTIVITY_SINGLE_TOP, another copy of the activity would be created and launched when the user presses the notification, so we have two copies in the stack

onNewIntent() - handling intents delivered to an activity

In the case above, in which we run an Intent when clicking the notification but do not relaunch the activity (due to FLAG_ACTIVITY_SINGLE_TOP), the activity will not be relaunched, but an Intent will still be delivered to it

  • We will, however, probably need to handle the Intent if it was delivered by a notification; it will probably contain useful information, such as the ID of an email message (we would want to display the email message with that ID)
  • The Activity class has an onNewIntent() method which will handle all Intents delivered to the activity, whether it's relaunched or not
  • This method takes the intent as a parameter, which we can then examine, e.g. by checking its action and then extracting the extras from it

For example, this responds to the PendingIntent we created earlier if the notification is launched from the same activity. Note how onNewIntent() takes the Intent as a parameter and we are testing its action. If the action is correct we extract the emailMessageId extra from the Intent.

Additional actions

Notifications can have additional actions (which appear as buttons). You use Notification.Builder's addAction() with an Action object to add these. The Action object requires an icon, text and a PendingIntent. For example, here we create a button "Stop Music Player" which is associated with a PendingIntent to stop the music (the details of how this is done will be covered in the Broadcasts topic).

Finally - adding click handlers to Ramani Maps objects

We haven't covered this yet, but this is a convenient place to introduce adding click handlers to Ramani Maps circles and symbols. To do this, use the onClick parameter and pass in a lambda as an event handler, e.g:

To use this you need to ensure that you use at least version 0.9.0 of Ramani Maps.

Exercise

This exercise builds on your Week 4 work (Ramani Maps with GPS). If you do not have this work, or you only have a version with navigation included, please clone this version:

  1. Modify the location listener so that it the app only receives a GPS update if they move at least 5 metres. This is to prevent too many notifications being generated in question 3, below.
  2. When the user clicks on the marker showing the current GPS position, display a dialog informing the user that the marker represents their current position. Ensure the dialog closes when it is dismissed.
  3. Modify the code so that a new notification is generated when the app receives a new GPS position.
  4. Modify your Topic 9 exercise solution so that a custom dialog is used to add a new song, rather than a separate composable accessible through navigation.