From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Slices provide the ability for your app to share
information and controls with other apps. In my previous post,
Share a Slice of Your App, I covered
the basics of Slices on Android. Introduced in Android Pie (API 28), they are packaged as a
library and work all the way back to Android KitKat (API 19). If you haven’t already, give it a read through to
learn about the main concepts in Slices, how to create a simple one, and how to view them on your
devices. Before continuing with this post, make sure you have a SliceProvider in place to build off
of for the interactive slices.
In my previous post, I covered adding multiple actions to your Slice to make it more
interactive. In this post, I’ll show how to add a toggle action. This type of action is good for
controls the user can enable or disable in your application. This type of control also requires you
to create a component to handle the data in your app. Read on to find out more!
For toggle actions, you will need to create either a Service
or a BroadcastReceiver
to handle
the updates. The Intent
object that these components receive will contain extra data indicating
if the toggle is enabled or not. You can handle the action appropriately there. For this example
I’ll use a Service
to handle the updates.
For now my Service
will be very plain. I’ll fill out the actual implementation once I finish my
Slice. This will just act as a placeholder so I can create a valid PendingIntent
.
class FocusService : IntentService("FocusService") {
override fun onHandleIntent(intent: Intent?) {
// Handle the toggle change
}
}
Don’t forget to register your service in your AndroidManifest.xml
so the system can start it
correctly.
The next step is to create a path for your toggle Slice. To continue with my creative streak, I’ll
call this path /toggle
.
override fun onBindSlice(sliceUri: Uri): Slice? {
context ?: return null
return when (sliceUri.path) {
"/toggle" -> {
// Display toggle slice
}
else -> {
...
}
}
}
To create my toggle action I can use the createToggle()
static method on the SliceAction
class.
This method needs a PendingIntent
, a title, and a boolean flag indicating if the toggle is
checked.
override fun onBindSlice(sliceUri: Uri): Slice? {
context ?: return null
return when (sliceUri.path) {
"/toggle" -> {
// Display toggle slice
val toggleAction = createToggleAction()
}
else -> {
...
}
}
}
private fun createToggleAction(): SliceAction {
val isFocusing = FocusManager.focusing
val focusIntent = Intent(context, FocusService::class.java).let {
PendingIntent.getService(context, 0, it, 0)
}
val toggleTitle = context.getString(R.string.toggle_title)
return SliceAction.createToggle(
focusIntent,
toggleTitle,
isFocusing
)
}
The FocusManager
is just an object responsible for determining the current state of my toggle.
The data is stored in a property that can be returned quickly back to my SliceProvider
.
object FocusManager {
public var focusing = false
}
Returning this value quickly is important so there isn’t a delay when your Slice is being
displayed. In fact, if you take too long to return the information, an exception is thrown, and your
Slice will not be shown. Even reading from SharedPreferences
is too long in this case. If reading
your data takes too long, you will need to initialize your Slice with placeholder data while you
query the real data in the background. Once you have the data, you can notify your SliceProvider
that the data has changed and it will rebind accordingly.
Once I have my toggle action, I can create the rest of the data for my Slice. I’ll change the text
of my Slice based on my toggle state. If my user is not focusing, I will prompt them to start, and
if they are focusing, I’ll prompt them to turn it off when they are done.
override fun onBindSlice(sliceUri: Uri): Slice? {
context ?: return null
return when (sliceUri.path) {
"/toggle" -> {
// Display toggle slice
val toggleAction = createToggleAction()
val focusTitleText: String
val focusSubtitleText: String
if (FocusManager.focusing) {
focusTitleText = context.getString(R.string.focus_title_text_enabled)
focusSubtitleText = context.getString(R.string.focus_subtitle_text_enabled)
} else {
focusTitleText = context.getString(R.string.focus_title_text_disabled)
focusSubtitleText = context.getString(R.string.focus_subtitle_text_disabled)
}
}
else -> {
...
}
}
}
With my action, title, and subtitle ready, I can move on to creating my Slice. I can add my toggle
action as my only action so the user can see it and interact with it.
override fun onBindSlice(sliceUri: Uri): Slice? {
context ?: return null
return when (sliceUri.path) {
"/toggle" -> {
...
} else {
focusTitleText = context.getString(R.string.focus_title_text_disabled)
focusSubtitleText = context.getString(R.string.focus_subtitle_text_disabled)
}
list(context, sliceUri, ListBuilder.INFINITY) {
header {
title = focusTitleText
subtitle = focusSubtitleText
}
addAction(toggleAction)
}
}
else -> {
...
}
}
}
The last step is to fill out my FocusService
to handle the toggle changes. I can pull out the
extra data from the intent and set it on my FocusManager
. I will also notify the system that the
content for my Slice URI has changed so that they system can tell my SliceProvider
to rebind my
Slice with the updated data.
class FocusService : IntentService("FocusService") {
override fun onHandleIntent(intent: Intent?) {
intent ?: return
val toggleState = if (intent.hasExtra(Slice.EXTRA_TOGGLE_STATE)) {
intent.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, false)
} else {
false
}
FocusManager.focusing = toggleState
val sliceUri = Uri.parse("content://com.bignerdranch.android.icingontheslice/toggle")
contentResolver.notifyChange(sliceUri, null)
}
}
With this in place, I can run my Slice and see the content change when I enable and disable the
toggle.
Toggle actions provide an easy way for users to enable or disable specific functionality in your
app in an easy to find place. You can either use a Service
or a BroadcastReceiver
to handle
the user’s input. Just configure a PendingIntent
for your component and the system will start it
when the data changes.
In the next post, I’ll cover how to add a range action to your Slice. This allows users to provide
a value from a discrete set of options.
I hope you enjoyed the post. If you have any questions please feel free to comment below. Thanks
for reading!
Introduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...