1. Overview
In the previous codelab, you used static shortcuts to implement commonly used built-in intents (BII) in a sample app. Android developers use App Actions to extend app functionality to Google Assistant.
Static shortcuts are bundled with an app and can only be updated by releasing new versions of the app. Enabling voice functionality for dynamic elements in an app, like user-generated content, is achieved using dynamic shortcuts. Apps push dynamic shortcuts after users perform relevant actions, like creating a new note in a task tracking app. With App Actions, you enable these shortcuts for voice by binding them to a BII, enabling users to access their content from Assistant by saying things like, "Hey Google, open my grocery list on ExampleApp."
Figure 1. Three progressive screens showing a user-created task, and Google Assistant launching a dynamic shortcut to that task item.
What you'll build
In this codelab, you'll enable dynamic shortcuts for voice in a sample to-do list Android app, enabling users to ask Assistant to open the task list items they create in the app. You accomplish this using using Android architecture patterns, specifically the repository, service locator and ViewModel patterns.
Prerequisites
This codelab builds on the App Actions concepts covered in the previous codelab, particularly BIIs and static shortcuts. If you are new to App Actions, we recommend completing that codelab before continuing.
Additionally, ensure that your development environment has the following configuration before proceeding:
- A terminal to run shell commands with git installed.
- The latest stable release of Android Studio.
- A physical or virtual Android device with Internet access.
- A Google account that is signed into Android Studio, the Google app, and the Google Assistant app.
2. Understand how it works
Enabling a dynamic shortcut for voice access with involves the following steps:
- Binding a dynamic shortcut to an eligible BII.
- Enabling Assistant to ingest the shortcuts by adding the Google Shortcuts Integration library.
- Pushing a shortcut whenever a user completes the relevant in-app task.
Binding shortcuts
For a dynamic shortcut to be accessible from Assistant, it needs to be bound to a relevant BII. When a BII with a shortcut is triggered, Assistant matches parameters in the user request to keywords defined in the bound shortcut. For example:
- A shortcut bound to the
GET_THING
BII could allow users to request specific in-app content, directly from Assistant. * "Hey Google, open my grocery list on ExampleApp." - A shortcut bound to the
START_EXERCISE
BII could allow users to see their exercise sessions. * "Hey Google, Ask ExampleApp to start my usual exercise."
Refer to the Built-in intents reference for a full categorized list of BIIs.
Providing shortcuts to Assistant
After binding your shortcuts to a BII, the next step is to enable Assistant to ingest these shortcuts by adding the Google Shortcuts Integration library to your project. With this library in place, Assistant will be aware of each shortcut pushed by your app, enabling users to launch those shortcuts by using the shortcut's trigger phrase in Assistant.
3. Prepare your development environment
This codelab uses a sample to-do list app built for Android. With this app, users can add items to lists, search for task list items by category, and filter tasks by completion status. Download and prepare the sample app by completing this section.
Download your base files
Run the following command to clone the sample app's GitHub repository:
git clone https://github.com/actions-on-google/app-actions-dynamic-shortcuts.git
Once you've cloned the repository, follow these steps to open it in Android Studio:
- In the Welcome to Android Studio dialog, click Import project.
- Select the folder where you cloned the repository.
Alternatively, you can view a version of the sample app representing the completed codelab by cloning the codelab-complete
branch of its Github repo:
git clone https://github.com/actions-on-google/app-actions-dynamic-shortcuts.git --branch codelab-complete
Update the Android application ID
Updating the app's application ID uniquely identifies the app on your test device and avoids a "Duplicate package name" error if the app is uploaded to the Play Console. To update the application ID, open app/build.gradle
:
android {
...
defaultConfig {
applicationId "com.MYUNIQUENAME.android.fitactions"
...
}
}
Replace "MYUNIQUENAME" in the applicationId
field to something unique to you.
Add Shortcuts API dependencies
Add the following Jetpack libraries to the app/build.gradle
resource file:
app/build.gradle
dependencies {
...
// Shortcuts library
implementation "androidx.core:core:1.6.0"
implementation 'androidx.core:core-google-shortcuts:1.0.1'
...
}
Test the app on your device
Before making more changes to the app, it's helpful to get an idea of what the sample app can do. To run the app on your emulator, follow these steps:
- In Android Studio, select Run > Run app or click Run in the toolbar.
- In the Select Deployment Target dialog, select a device and click OK. The recommended OS version is Android 10 (API level 30) or higher, although App Actions works on devices back to Android 5 (API level 21).
- Long-press on the Home button to set up Assistant and verify that it works. You will need to sign in to Assistant on your device, if you haven't already.
For more information on Android virtual devices, see Create and manage virtual devices.
Briefly explore the app to see what it can do. Tapping the Plus icon creates a new task item, and the menu items on the top right allow you to search for and filter task items by completion status.
4. Create a shortcut repository class
Several classes in our sample app will call the ShortcutManagerCompat
API to push and manage dynamic shortcuts. To reduce code redundancy, you will implement a repository to enable your project classes to easily manage dynamic shortcuts.
The repository design pattern provides a clean API for managing shortcuts. The advantage to a repository is the details of the underlying API are uniformly abstracted behind a minimal API. Implement the repository by following these steps:
- Create a
ShortcutsRepository
class to abstract theShortcutManagerCompat
API. - Add
ShortcutsRepository
methods to the app's service locator. - Register the
ShortcutRepository
service in the main Application.
Create the repository
Create a new Kotlin class named ShortcutsRepository
in the com.example.android.architecture.blueprints.todoapp.data.source
package. You can find this package organized in the app/src/main/java
folder. You will use this class to implement an interface providing a minimal set of methods covering our codelab use case.
Figure 2. Android Studio Project Files window displaying the location of the ShortcutsRepository class.
Paste the following code into the new class:
package com.example.android.architecture.blueprints.todoapp.data.source
import android.content.Context
import android.content.Intent
import androidx.annotation.WorkerThread
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity
private const val GET_THING_KEY = "q"
/**
* ShortcutsRepository provides an interface for managing dynamic shortcuts.
*/
class ShortcutsRepository(val context: Context) {
private val appContext = context.applicationContext
/**
* Pushes a dynamic shortcut. The task ID is used as the shortcut ID.
* The task's title and description are used as shortcut's short and long labels.
* The resulting shortcut corresponds to the GET_THING capability with task's
* title used as BII's "name" argument.
*
* @param task Task object for which to create a shortcut.
*/
@WorkerThread
fun pushShortcut(task: Task) {
// TODO
}
private fun createShortcutCompat(task: Task): ShortcutInfoCompat {
//...
}
/**
* Updates a dynamic shortcut for the provided task. If the shortcut
* associated with this task doesn't exist, this method throws an error.
* This operation may take a few seconds to complete.
*
* @param tasks list of tasks to update.
*/
@WorkerThread
fun updateShortcuts(tasks: List<Task>) {
//...
}
/**
* Removes shortcuts if IDs are known.
*
* @param ids list of shortcut IDs
*/
@WorkerThread
fun removeShortcutsById(ids: List<String>) {
//...
}
/**
* Removes shortcuts associated with the tasks.
*
* @param tasks list of tasks to remove.
*/
@WorkerThread
fun removeShortcuts(tasks: List<Task>) {
//...
}
}
Next, update the pushShortcut
method to call the ShortcutManagerCompat
API. Update the ShortcutsRepository
class with the following code:
ShortcutsRepository.kt
/**
* Pushes a dynamic shortcut for the task. The task's ID is used as a shortcut
* ID. The task's title and description are used as shortcut's short and long
* labels. The created shortcut corresponds to GET_THING capability with task's
* title used as BII's "name" argument.
*
* @param task Task object for which to create a shortcut.
*/
@WorkerThread
fun pushShortcut(task: Task) {
ShortcutManagerCompat.pushDynamicShortcut(appContext, createShortcutCompat(task))
}
In the preceding code sample we passed appContext
to the API. This is a class property holding an Application Context. It's important to use an Application Context (as opposed to an Activity Context) to avoid memory leaks, since the context might be retained longer than the host activity lifecycle.
Additionally, the API requires that we pass a ShortcutInfoCompat
object for the Task object. In the preceding code sample we accomplish this by by calling the createShortcutCompat
private method, which we will update to create and return a ShortcutInfoCompat
object. To accomplish this, update the createShortcutCompat
stub with the following code:
ShortcutsRepository.kt
private fun createShortcutCompat(task: Task): ShortcutInfoCompat {
val intent = Intent(appContext, TasksActivity::class.java)
intent.action = Intent.ACTION_VIEW
// Filtering is set based on currentTitle.
intent.putExtra(GET_THING_KEY, task.title)
// A unique ID is required to avoid overwriting an existing shortcut.
return ShortcutInfoCompat.Builder(appContext, task.id)
.setShortLabel(task.title)
.setLongLabel(task.title)
// Call addCapabilityBinding() to link this shortcut to a BII. Enables user to invoke a shortcut using its title in Assistant.
.addCapabilityBinding(
"actions.intent.GET_THING", "thing.name", listOf(task.title))
.setIntent(intent)
.setLongLived(false)
.build()
}
The remaining function stubs in this class deal with updating and deleting dynamic shortcuts. Enable these functions by updating them with the following code:
ShortcutsRepository.kt
/**
* Updates a Dynamic Shortcut for the task. If the shortcut associated with this task
* doesn't exist, throws an error. This operation may take a few seconds to complete.
*
* @param tasks list of tasks to update.
*/
@WorkerThread
fun updateShortcuts(tasks: List<Task>) {
val scs = tasks.map { createShortcutCompat(it) }
ShortcutManagerCompat.updateShortcuts(appContext, scs)
}
/**
* Removes shortcuts if IDs are known.
* @param ids list of shortcut IDs
*/
@WorkerThread
fun removeShortcutsById(ids: List<String>) {
ShortcutManagerCompat.removeDynamicShortcuts(appContext, ids)
}
/**
* Removes shortcuts associated with the tasks.
*
* @param tasks list of tasks to remove.
*/
@WorkerThread
fun removeShortcuts(tasks: List<Task>) {
ShortcutManagerCompat.removeDynamicShortcuts (appContext,
tasks.map { it.id })
}
Add class to service locator
With the ShortcutsRepository
class created, the next step is to make instantiated objects of this class available to the rest of the app. This app manages class dependencies by implementing the service locator pattern. Open the service locator class using the class browser in Android Studio by going to Navigate > Class and typing "ServiceLocator". Click the resulting Kotlin file to open it in your IDE.
At the top of ServiceLocator.kt
, paste the following code to import the ShortcutsRepository
and SuppressLint
packages:
ServiceLocator.kt
package com.example.android.architecture.blueprints.todoapp
// ...Other import statements
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository
import android.annotation.SuppressLint
Add the ShortcutRepository
service members and methods by pasting the following code into the body of ServiceLocator.kt
:
ServiceLocator.kt
object ServiceLocator {
// ...
// Only the code immediately below this comment needs to be copied and pasted
// into the body of ServiceLocator.kt:
@SuppressLint("StaticFieldLeak")
@Volatile
var shortcutsRepository: ShortcutsRepository? = null
private fun createShortcutsRepository(context: Context): ShortcutsRepository {
val newRepo = ShortcutsRepository(context.applicationContext)
shortcutsRepository = newRepo
return newRepo
}
fun provideShortcutsRepository(context: Context): ShortcutsRepository {
synchronized(this) {
return shortcutsRepository ?: shortcutsRepository ?: createShortcutsRepository(context)
}
}
}
Register the shortcut service
The final step is to register your new ShortcutsRepository
service with the Application. In Android Studio, open TodoApplication.kt
and copy the following code near the top of the file:
TodoApplication.kt
package com.example.android.architecture.blueprints.todoapp
/// ... Other import statements
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository
Next, register the service by adding the following code to the body of the class:
TodoApplication.kt
//...
class TodoApplication : Application() {
//...
val shortcutsRepository: ShortcutsRepository
get() = ServiceLocator.provideShortcutsRepository(this)
//...
}
Build the app and make sure that it continues to run.
5. Push a new shortcut
With your shortcut service created, you're ready to start pushing shortcuts. Since users generate content (task items) in this app and expect to be able to return to them later, we will voice-enable access to this content by pushing a dynamic shortcut that is bound to the GET_THING
BII each time a user creates a new task. This enables Assistant to launch users directly to their requested task item when they trigger the BII by asking things like, "Hey Google, open my grocery list on SampleApp."
You enable this functionality in the sample app by completing these steps:
- Importing the
ShortcutsRepository
service to theAddEditTaskViewModel
class, responsible for managing task list objects. - Pushing a dynamic shortcut when the user creates a new task.
Import ShortcutsRepository
We first need to make the ShortcutsRepository
service available to AddEditTaskViewModel
. To accomplish this, import the service to ViewModelFactory
, the factory class the app uses to instantiate ViewModel objects, including AddEditTaskViewModel
.
Open the class browser in Android Studio by going to Navigate > Class and typing "ViewModelFactory". Click the resulting Kotlin file to open it in your IDE.
At the top of ViewModelFactory.kt
, paste the following code to import the ShortcutsRepository
and SuppressLint
packages:
ViewModelFactory.kt
package com.example.android.architecture.blueprints.todoapp
// ...Other import statements
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository
Next, replace the body of ViewModelFactory
with the following code:
ViewModelFactory.kt
/**
* Factory for all ViewModels.
*/
@Suppress("UNCHECKED_CAST")
class ViewModelFactory constructor(
private val tasksRepository: TasksRepository,
private val shortcutsRepository: ShortcutsRepository,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = with(modelClass) {
when {
isAssignableFrom(StatisticsViewModel::class.java) ->
StatisticsViewModel(tasksRepository)
isAssignableFrom(TaskDetailViewModel::class.java) ->
TaskDetailViewModel(tasksRepository)
isAssignableFrom(AddEditTaskViewModel::class.java) ->
AddEditTaskViewModel(tasksRepository, shortcutsRepository)
isAssignableFrom(TasksViewModel::class.java) ->
TasksViewModel(tasksRepository, handle)
else ->
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
Finish the ViewModelFactory
changes by going one layer up, and pass ShortcutsRepository
to the factory's constructor. Open Android Studio's file browser by going to Navigate > File and typing "FragmentExt.kt". Click the resulting Kotlin file located in the util package to open it in your IDE.
Replace the body of FragmentExt.kt
with the following code:
fun Fragment.getViewModelFactory(): ViewModelFactory {
val taskRepository = (requireContext().applicationContext as TodoApplication).taskRepository
val shortcutsRepository = (requireContext().applicationContext as TodoApplication).shortcutsRepository
return ViewModelFactory(taskRepository, shortcutsRepository, this)
}
Push a shortcut
With the ShortcutsRepository
abstraction class available to the sample app's ViewModel
classes, you update AddEditTaskViewModel
, the ViewModel
class responsible for creating notes, to push a dynamic shortcut each time a user creates a new note.
In Android Studio, open the class browser and type "AddEditTaskViewModel". Click the resulting Kotlin file to open it in your IDE.
First, add the ShortcutsRepository
package to this class with the following import statement:
package com.example.android.architecture.blueprints.todoapp.addedittask
//Other import statements
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository
Next, add the shortcutsRepository
class property by updating the class constructor with the following code:
AddEditTaskViewModel.kt
//...
/**
* ViewModel for the Add/Edit screen.
*/
class AddEditTaskViewModel(
private val tasksRepository: TasksRepository,
private val shortcutsRepository: ShortcutsRepository
) : ViewModel() {
//...
With the ShortcutsRepository
class added, create a new function, pushShortcut()
, to call this class. Paste the following private function into the body of AddEditTaskViewModel
:
AddEditTaskViewModel.kt
//...
private fun pushShortcut(newTask: Task) = viewModelScope.launch {
shortcutsRepository.pushShortcut(newTask)
}
Finally, push a new dynamic shortcut whenever a task gets created. Replace the contents of the saveTask()
function with the following code:
AddEditTaskViewModel.kt
fun saveTask() {
val currentTitle = title.value
val currentDescription = description.value
if (currentTitle == null || currentDescription == null) {
_snackbarText.value = Event(R.string.empty_task_message)
return
}
if (Task(currentTitle, currentDescription).isEmpty) {
_snackbarText.value = Event(R.string.empty_task_message)
return
}
val currentTaskId = taskId
if (isNewTask || currentTaskId == null) {
val task = Task(currentTitle, currentDescription)
createTask(task)
pushShortcut(task)
} else {
val task = Task(currentTitle, currentDescription, taskCompleted, currentTaskId)
updateTask(task)
}
}
Test your code
We're finally ready to test our code! In this step, you push a voice enabled dynamic shortcut and inspect it using the Google Assistant app.
Create a preview
Creating a preview using the Google Assistant plugin enables your dynamic shortcuts to appear in Assistant on your test device.
Install the test plugin
If you do not already have the Google Assistant plugin, install it by following these steps in Android Studio:
- Go to **File > Settings (Android Studio > Preferences on MacOS).
- In the Plugins section, go to Marketplace and search for "Google Assistant".
- If you cannot find the plugin on the Marketplace, download the plugin manually and follow the instructions on Install the plugin from disk.
- Install the tool and restart Android Studio.
Create the preview
Create a preview by following these steps in Android Studio:
- Click on Tools > Google Assistant > "App Actions Test Tool".
- In the App name box, define a name like "Todo List".
- Click Create Preview. If asked, review and accept the App Actions policies and terms of service.
Figure 3. The App Actions Test Tool preview creation pane.
During testing, dynamic shortcuts you push to Assistant will appear in Assistant organized by the App name you provided for the preview.
Push and inspect a shortcut
Relaunch the sample app on your test device and perform the following steps :
- Create a new task with the title "Start codelab"
- Open Google Assistant app and say or type: "My shortcuts."
- Tap the Explore tab. You should see the sample shortcut.
- Tap the shortcut to invoke it. You should see the app launch with the name of the shortcut pre-populated in the filter box, making it easy to find the requested task item.
6. (Optional) Update and delete a shortcut
In addition to pushing new dynamic shortcuts at runtime, your app can update them to reflect the current state of your user content and preferences. It's good practice to update existing shortcuts whenever a user modifies the destination item, like renaming a task in our sample app. You should also delete a corresponding shortcut whenever the destination resource is removed to avoid displaying broken shortcuts to user.
Update a shortcut
Modify AddEditTaskViewModel
to update a dynamic shortcut whenever a user changes the details of a task item. First, update the body of the class with the following code to add an update function that utilizes our repository class:
AddEditTaskViewModel.kt
private fun updateShortcut(newTask: Task) = viewModelScope.launch {
shortcutsRepository.updateShortcuts(listOf(newTask))
}
Next, modify the saveTask()
function to call our new method whenever an existing task is updated.
AddEditTaskViewModel.kt
// Called when clicking on fab.
fun saveTask() {
// ...
// Note: the shortcuts are created/updated in a worker thread.
if (isNewTask || currentTaskId == null) {
//...
} else {
//...
updateShortcut(task)
}
}
Test your code by relaunching the app and following these steps:
- Rename the title of your existing task item to "Finish codelab".
- Open the Google Assistant by saying, "Hey Google, my shortcuts."
- Tap the Explore tab. You should see an updated short label for your test Shortcut.
Remove a shortcut
Our sample app shortcuts should be removed whenever a user deletes a task. In the sample app, task deletion logic lives in the TaskDetailViewModel
class. Before we update this class, we need to update ViewModelFactory
again to pass shortcutsRepository
into TaskDetailViewModel
.
Open ViewModelFactory
and replace the contents of its constructor method with the following code:
//...
class ViewModelFactory constructor(
private val tasksRepository: TasksRepository,
private val shortcutsRepository: ShortcutsRepository,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = with(modelClass) {
when {
isAssignableFrom(StatisticsViewModel::class.java) ->
StatisticsViewModel(tasksRepository)
isAssignableFrom(TaskDetailViewModel::class.java) ->
TaskDetailViewModel(tasksRepository, shortcutsRepository)
isAssignableFrom(AddEditTaskViewModel::class.java) ->
AddEditTaskViewModel(tasksRepository, shortcutsRepository)
isAssignableFrom(TasksViewModel::class.java) ->
TasksViewModel(tasksRepository, handle)
else ->
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
Next, open TaskDetailViewModel
. Import the ShortcutsRepository
module and declare an instance variable for it using the following code:
TaskDetailViewModel.kt
package com.example.android.architecture.blueprints.todoapp.taskdetail
...
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository
/**
* ViewModel for the Details screen.
*/
class TaskDetailViewModel(
//...
private val shortcutsRepository: ShortcutsRepository
) : ViewModel() {
...
}
Finally, modify the deleteTask()
function to call shortcutsRepository
to remove a shortcut based on its ID whenever a task with a corresponding taskId
is deleted:
TaskDetailViewModel.kt
fun deleteTask() = viewModelScope.launch {
_taskId.value?.let {
//...
shortcutsRepository.removeShortcutsById(listOf(it))
}
}
To test your code, relaunch the app and follow these steps:
- Delete your test task.
- Rename the title of your existing task item to "Finish codelab".
- Open the Google Assistant by saying, "Hey Google, my shortcuts."
- Tap the Explore tab. Confirm that your test shortcut no longer appears.
7. Next steps
Congratulations! Thanks to you, users of our sample app can easily return to the notes they create by asking Assistant things like, "Hey Google, open my grocery list on ExampleApp". Shortcuts encourage deeper user engagement by making it easy for users to replay frequently used actions in your app.
What we've covered
In this codelab, you learned how to:
- Identify use cases for pushing dynamic shortcuts in an app.
- Reduce code complexity using repository, dependency injection and service locator design patterns.
- Push voice-enabled dynamic shortcuts to user-generated app content.
- Update and remove existing shortcuts.
What's next
From here, you can try making further refinements to your Task List app. To reference the finished project, see the repo –codelab-complete branch on GitHub.
Here are some suggestions for further learning about extending this app with App Actions:
- Check out the To-do list sample with Google Analytics for Firebase to learn how to track the performance of your App Actions.
- Visit the App Actions built-in intents reference to discover more ways to extend your apps to Assistant.
To continue your Actions on Google journey, explore these resources:
- actions.google.com: Official documentation site for Actions on Google.
- App Actions sample index: Sample apps and code for exploring App Actions capabilities.
- Actions on Google GitHub repo: Sample code and libraries.
- r/GoogleAssistantDev: Official Reddit community for developers working with Google Assistant.
Follow us on Twitter @ActionsOnGoogle to stay tuned to our latest announcements, and tweet to #appActions to share what you have built!
Feedback survey
Finally, please fill out this survey to give feedback about your experience with this codelab.