App Actions を使用して動的ショートカットを Google アシスタントに拡張する

1. 概要

前回の Codelab では、静的ショートカットを使用して、一般的な組み込みインテント(BII)をサンプルアプリに実装しました。Android アプリの開発では、アプリの機能を Google アシスタントへと拡張するのに App Actions が使用されます。

静的ショートカットはひとつのアプリとバンドルされており、内容をアップデートするにはアプリの新バージョンをリリースする必要があります。アプリ内の動的な要素(たとえばユーザー作成コンテンツ)を音声で扱う機能を実現するには、動的ショートカットを使用します。動的ショートカットは、ユーザーが関連する操作(タスク トラッキング アプリで新しいメモを作成するなど)を行うたびに、アプリによってプッシュされます。こういったショートカットを App Actions で BII にバインドすれば、音声での操作に対応させることが可能です。これによりユーザーは Google アシスタントを介して、たとえば「OK Google, ExampleApp で買い物リストを開いて」と言うことで、自分が作成したコンテンツにアクセスできるようになります。

Google アシスタントが動的ショートカットを起動する様子を、3 枚の連続画像で示したもの。

図 1. ユーザーが作成したタスクと、Google アシスタントがそのタスクアイテムへの動的ショートカットを起動する様子を、3 枚の連続画像で示したもの。

作成する内容

この Codelab では、サンプルの Android 向け To-Do リスト アプリで動的ショートカットを音声操作に対応させ、ユーザーがアプリ内で自作したタスクリスト アイテムを、Google アシスタントへの音声指示で開けるようにします。この作業は、Android のアーキテクチャ パターン、特にリポジトリサービス ロケータViewModel を使用して行います。

前提条件

この Codelab は、前回の Codelab で扱った App Actions のコンセプト、特に BII と静的ショートカットの理解を前提とした内容です。App Actions をあまり扱ったことがない場合は、先に前回の Codelab を完了されることをおすすめします。

また、続行する前に、開発環境に以下が揃っていることを確認しましょう。

  • シェルコマンドを実行するためのターミナル(git がインストールされていること)。
  • Android Studio の最新の安定版。
  • インターネットにアクセスできる、Android の実機または仮想デバイス。
  • Android Studio、Google アプリ、Google アシスタント アプリにログイン済みの Google アカウント。

2. 仕組みを理解する

動的ショートカットを音声アクセスに対応させるには、以下を行う必要があります。

  • 動的ショートカットを適切な BII にバインドする
  • Google アシスタントがショートカットを処理できるよう、Google Shortcuts Integration ライブラリを追加する
  • 関連するアプリ内タスクをユーザーが完了するたびにショートカットをプッシュする

ショートカットをバインドする

動的ショートカットを Google アシスタントからアクセス可能にするには、対応する BII にバインドしておく必要があります。ショートカットをバインド済みの BII がトリガーされると、Google アシスタントはユーザー リクエスト内のパラメータを、バインド先のショートカットで定義されたキーワードとマッチングします。たとえば次のようになります。

  • BII「GET_THING」にバインド済みのショートカットによって、特定のアプリ内コンテンツを Google アシスタントから直接リクエスト可能に: 「OK Google, ExampleApp で買い物リストを開いて」
  • BII「ORDER_MENU_ITEM」にバインド済みのショートカットによって過去の注文を再現可能に: 「OK Google, ExampleApp からいつもの商品を注文して」

全 BII のリスト(カテゴリ別)は、組み込みインテントのリファレンスでご確認いただけます。

ショートカットを Google アシスタントに提供する

ショートカットを BII にバインドしたら、次は Google アシスタントがショートカットを処理できるよう、プロジェクトに Google Shortcuts Integration ライブラリを追加します。このライブラリを導入すると、アプリがプッシュするショートカットを Google アシスタントが認識し、ユーザーは Google アシスタントでショートカットのトリガー フレーズを使用することにより該当ショートカットを起動できるようになります。

3. 開発環境を準備する

この Codelab では、サンプルの Android 向け To-Do リスト アプリを使用します。このアプリでは、ユーザーがリストにアイテムを追加したり、タスクリストのアイテムをカテゴリ別で検索したり、完了ステータス別でタスクをフィルタしたりすることができます。本セクションではサンプルアプリのダウンロードと準備を行います。

ベースファイルをダウンロードする

次のコマンドを実行して、サンプルアプリの GitHub リポジトリのクローンを作成します。

git clone https://github.com/actions-on-google/app-actions-dynamic-shortcuts.git

リポジトリのクローンを作成したら、次の手順に沿って Android Studio で開きます。

  1. [Welcome to Android Studio] ダイアログで、[Import project] をクリックします。
  2. リポジトリのクローンを作成したフォルダを選択します。

Codelab の手順を終えた状態に相当するバージョンのサンプルアプリを参照することも可能です。この場合、次のコマンドで GitHub リポジトリの codelab-complete ブランチのクローンを作成します。

git clone https://github.com/actions-on-google/app-actions-dynamic-shortcuts.git --branch codelab-complete

Android アプリケーション ID を更新する

アプリのアプリケーション ID を更新すると、テストデバイス上のアプリが一意に識別され、Google Play Console にアプリをアップロードする場合に「パッケージ名の重複」エラーが発生するのを回避できます。アプリケーション ID を更新するには、app/build.gradle を開きます。

android {
...
  defaultConfig {
    applicationId "com.MYUNIQUENAME.android.fitactions"
    ...
  }
}

applicationId フィールドの「MYUNIQUENAME」を独自の値に置き換えます。

ショートカット API の依存関係を追加する

次の Jetpack ライブラリを、app/build.gradle リソース ファイルに追加します。

app/build.gradle

dependencies {
   ...
   // Shortcuts library
   implementation "androidx.core:core:1.6.0"
   implementation 'androidx.core:core-google-shortcuts:1.0.1'
   ...
}

デバイスでアプリをテストする

アプリの編集をさらに進める前に、サンプルアプリでどんなことができるのか把握しておくといいでしょう。エミュレータでアプリを実行する手順は次のとおりです。

  1. Android Studio で、[Run] > [Run app] を選択するか、ツールバーの実行アイコン Android Studio のアプリ実行アイコン をクリックします。
  2. [Select Deployment Target] ダイアログでデバイスを選択し、[OK] をクリックします。推奨される OS バージョンは Android 10(API レベル 30)以上ですが、App Actions は Android 5(API レベル 21)以上のデバイスであれば動作します。
  3. ホームボタンを長押しして Google アシスタントをセットアップし、正しく機能することを確認します。デバイスで Google アシスタントにログインします(していなかった場合)。

Android Virtual Device について詳しくは、仮想デバイスを作成して管理するをご覧ください。

アプリを少し操作して、何ができるのか確認してみましょう。プラスアイコンをタップすると、新しいタスクアイテムを作成できます。右上のメニュー アイテムを使用すると、完了ステータスに基づいてタスクアイテムを検索およびフィルタできます。

4. ショートカット リポジトリ クラスを作成する

サンプルアプリのクラスの中には、動的ショートカットのプッシュと管理のために ShortcutManagerCompat API を呼び出すものがいくつかあります。コードの重複を減らすため、リポジトリを実装して、プロジェクトの各クラスが動的ショートカットを管理しやすいようにしましょう。

リポジトリ型のデザイン パターンでは、ショートカットの管理に明快な API を利用できます。リポジトリの利点は、基盤となる API の詳細が一律に抽象化され、ミニマルな API の背後に隠れることです。リポジトリを実装する手順は次のとおりです。

  1. ShortcutManagerCompat API を抽象化するため、ShortcutsRepository クラスを作成します。
  2. アプリのサービス ロケータに、ShortcutsRepository メソッド群を追加します。
  3. メインのアプリケーションで ShortcutRepository サービスを登録します。

リポジトリを作成する

com.example.android.architecture.blueprints.todoapp.data.source パッケージ内に、ShortcutsRepository という名前の新しい Kotlin クラスを作成します。このパッケージは、app/src/main/java フォルダに整理されています。このクラスを使って、Codelab のユースケースに必要な最小限のメソッド群を提供するインターフェースを実装します。

Android Studio のウィンドウに ShortcutsRepository クラスの場所が表示されている様子。

図 2. Android Studio の [Project Files] ウィンドウに ShortcutsRepository クラスの場所が表示されている様子。

作成したクラスに、次のコードを貼り付けます。

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>) {
       //...
   }
}

次に、pushShortcut メソッドに変更を加えて ShortcutManagerCompat API を呼び出すようにします。ShortcutsRepository クラスを次のように変更しましょう。

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))
}

上のコードサンプルでは、API に appContext を渡しています。これは、アプリケーション コンテキストを持つクラス プロパティです。コンテキストはホストのアクティビティ ライフサイクルよりも長く維持される可能性があるため、メモリリークを防ぐには(アクティビティ コンテキストではなく)アプリケーション コンテキストを使用することが重要です。

また、この API では Task オブジェクトに対応する ShortcutInfoCompat オブジェクトを渡す必要があります。上のコードサンプルでは、プライベート メソッド createShortcutCompat を呼び出すことでこれを実現しています。createShortcutCompat の部分はスタブになっており、ShortcutInfoCompat オブジェクトを作成して返すように変更する必要があります。次のコードを使用しましょう。

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()
}

クラス内の残りの関数スタブは、動的ショートカットの更新と削除を扱うものです。各関数を次のように完成させましょう。

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 })
}

クラスをサービス ロケータに追加する

ShortcutsRepository クラスが作成できたので、次はこのクラスをインスタンス化したオブジェクトを、アプリの他の部分から利用可能にします。このアプリでは、サービス ロケータ パターンを実装することによってクラスの依存関係を管理しています。Android Studio のクラスブラウザで、サービス ロケータ クラスを開きましょう。[Navigate] > [Class] を開き、「ServiceLocator」と入力します。表示された Kotlin ファイルをクリックすると、IDE で開くことができます。

ServiceLocator.kt の先頭に次のコードを貼り付けて、ShortcutsRepositorySuppressLint のパッケージをインポートします。

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

ShortcutRepository サービスのメンバーとメソッドを追加します。次のコードを 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)
       }
   }
 }

ショートカット サービスを登録する

最後に、作成した ShortcutsRepository サービスをアプリケーション本体で登録します。Android Studio で TodoApplication.kt を開いて、次のコードをファイルの先頭近くに貼り付けましょう。

TodoApplication.kt

package com.example.android.architecture.blueprints.todoapp
/// ... Other import statements

import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository

次に、以下のコードをクラスの本文に追加して、サービスを登録します。

TodoApplication.kt

//...
class TodoApplication : Application() {

   //...

   val shortcutsRepository: ShortcutsRepository
       get() = ServiceLocator.provideShortcutsRepository(this)

   //...
}

アプリをビルドして、問題なく動作することを確認します。

5. 新しいショートカットをプッシュする

ショートカット サービスの作成が完了したので、ショートカットをプッシュできるようになりました。このアプリでは、ユーザーは自分が作成したコンテンツ(タスクアイテム)に後でアクセスすることが想定されます。この作業を音声での操作に対応させるため、ユーザーが新しいタスクを作成するたびに、BII「GET_THING」にバインドされた動的ショートカットがプッシュされるようにしましょう。これにより Google アシスタントは、「OK Google, SampleApp で買い物リストを開いて」などの音声指示で BII がトリガーされた場合に、ユーザーを要求されたタスクアイテムへ直接案内できるようになります。

サンプルアプリでこの機能を実現するには、以下を行う必要があります。

  1. タスクリストのオブジェクトを管理する AddEditTaskViewModel クラスに ShortcutsRepository サービスをインポートする
  2. ユーザーが新しいタスクを作成した際に動的ショートカットがプッシュされるようにする

ShortcutsRepository をインポートする

まず、AddEditTaskViewModelShortcutsRepository サービスを利用できるようにする必要があります。アプリが AddEditTaskViewModel などの ViewModel オブジェクトをインスタンス化するのに使うファクトリー クラス「ViewModelFactory」に、同サービスをインポートしましょう。

Android Studio でクラスブラウザを開き([Navigate] > [Class])、「ViewModelFactory」と入力します。表示された Kotlin ファイルをクリックすると、IDE で開くことができます。

ViewModelFactory.kt の先頭に次のコードを貼り付けて、ShortcutsRepositorySuppressLint のパッケージをインポートします。

ViewModelFactory.kt

package com.example.android.architecture.blueprints.todoapp

// ...Other import statements
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository

次に、ViewModelFactory の本文を以下のコードに差し替えます。

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
}

ひとつ上のレイヤに移動して ViewModelFactory の変更を終了させ、ShortcutsRepository をファクトリーのコンストラクタに渡します。Android Studio でファイル ブラウザを開き([Navigate] > [File])、「FragmentExt.kt」と入力します。表示された Kotlin ファイル(util パッケージ内)をクリックすると、IDE で開くことができます。

FragmentExt.kt の本文を以下のコードに差し替えます。

fun Fragment.getViewModelFactory(): ViewModelFactory {
   val taskRepository = (requireContext().applicationContext as TodoApplication).taskRepository
   val shortcutsRepository = (requireContext().applicationContext as TodoApplication).shortcutsRepository
   return ViewModelFactory(taskRepository, shortcutsRepository, this)
}

ショートカットをプッシュする

サンプルアプリの ViewModel クラス群が抽象化クラス ShortcutsRepository を利用できるようになったので、次は AddEditTaskViewModel(メモの作成を担当する ViewModel クラス)に変更を加えて、ユーザーが新しいメモを作成するたびに動的ショートカットがプッシュされるようにします。

Android Studio でクラスブラウザを開き、「AddEditTaskViewModel」と入力します。表示された Kotlin ファイルをクリックすると、IDE で開くことができます。

まず、次のインポート文で、このクラスに ShortcutsRepository パッケージを追加します。

package com.example.android.architecture.blueprints.todoapp.addedittask

//Other import statements
import com.example.android.architecture.blueprints.todoapp.data.source.ShortcutsRepository

次に、クラス コンストラクタのコードを次のように変更して、shortcutsRepository クラス プロパティを追加します。

AddEditTaskViewModel.kt

//...

/**
* ViewModel for the Add/Edit screen.
*/
class AddEditTaskViewModel(
   private val tasksRepository: TasksRepository,
   private val shortcutsRepository: ShortcutsRepository
) : ViewModel() {

    //...

ShortcutsRepository クラスを追加できたので、このクラスを呼び出す新しい関数 pushShortcut() を作成します。次のプライベート関数を、AddEditTaskViewModel の本文内に貼り付けましょう。

AddEditTaskViewModel.kt

//...
private fun pushShortcut(newTask: Task) = viewModelScope.launch {
   shortcutsRepository.pushShortcut(newTask)
}

最後に、タスクが作成されるたびに新しい動的ショートカットがプッシュされるようにします。saveTask() 関数の内容を次のコードに差し替えましょう。

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)
    }
}

コードをテストする

いよいよコードをテストできる段階になりました。このステップでは Google アシスタント アプリを使用して、音声対応の動的ショートカットをプッシュし、検査します。

プレビューを作成する

Google アシスタント プラグインを使ってプレビューを作成することにより、テストデバイスの Google アシスタントで動的ショートカットを確認できます。

テスト プラグインをインストールする

Google アシスタント プラグインをまだお持ちでない場合は、Android Studio で以下の手順を行ってインストールします。

  1. **[File] > [Settings] を開きます(macOS の場合は [Android Studio] > [Preferences])。
  2. [Plugins] セクションで [Marketplace] に移動して「Google Assistant」を検索します。
  3. ツールをインストールして Android Studio を再起動します。

プレビューを作成する

Android Studio でプレビューを作成する手順は次のとおりです。

  1. [Tools] > [Google Assistant] > [App Actions Test Tool] をクリックします。
  2. [App name] ボックスで名前を付けます(「To-Do リスト」など)。
  3. [Create Preview] をクリックします。求められた場合は、App Actions のポリシーと利用規約を確認して同意します。

App Actions Test Tool のプレビュー作成ペイン。

図 3. App Actions Test Tool のプレビュー作成ペイン。

テスト中、Google アシスタントへプッシュした動的ショートカットを、アシスタント内で確認できます。ショートカットはプレビュー作成時に入力したアプリ名(App name)別で表示されます。

ショートカットのプッシュと検査を行う

テストデバイスでサンプルアプリを再起動して、以下を行います。

  1. 「Start Codelab」というタイトルのタスクを作成します。
  2. Google アシスタント アプリを開いて、「My shortcuts」と言うか入力します。
  3. 使い方・ヒント」タブをタップします。サンプル ショートカットが表示されているはずです。
  4. ショートカットをタップして起動します。アプリが起動するはずです。リクエストされたタスクアイテムを見つけやすいよう、フィルタ ボックスにはショートカット名があらかじめ入力されています。

6. (任意)ショートカットの更新と削除を行う

アプリは実行時に新しい動的ショートカットをプッシュするだけでなく、ユーザーのコンテンツや設定の現状を反映するために動的ショートカットを更新することもできます。ユーザーが対象アイテムを変更したとき(今回使用しているサンプルアプリならタスクの名称を変更した場合など)は、その都度既存のショートカットが更新されるようにするのがいいでしょう。また、対象リソースが存在しなくなったときは、壊れたショートカットがユーザーに表示されないよう、対応するショートカットを削除する必要があります。

ショートカットを更新する

AddEditTaskViewModel に変更を加え、ユーザーがタスクアイテムの詳細を変更するたびに、動的ショートカットが更新されるようにしましょう。まず、同クラスの本文を次のように変更して、作成したリポジトリ クラスを使ってショートカットを更新する関数を追加します。

AddEditTaskViewModel.kt

private fun updateShortcut(newTask: Task) = viewModelScope.launch {
   shortcutsRepository.updateShortcuts(listOf(newTask))
}

次に、saveTask() 関数に変更を加えて、既存のタスクが編集されるたびに、作成したメソッドを呼び出すようにします。

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)
   }
}

コードをテストするため、アプリを再起動して以下を行います。

  1. 既存のタスクアイテムのタイトルを「Finish Codelab」に変更します。
  2. 「OK Google, my shortcuts」と音声で指示して Google アシスタントを開きます。
  3. 使い方・ヒント」タブをタップします。テスト用ショートカットに、更新済みの短いラベルが表示されているはずです。

ショートカットを削除する

今回のサンプルアプリでは、ユーザーがタスクを削除するたびに、対応するショートカットも削除する必要があります。サンプルアプリのタスク削除のロジックは TaskDetailViewModel クラスに格納されています。このクラスに変更を加える前に、ViewModelFactory を再度編集して、shortcutsRepositoryTaskDetailViewModel に渡すようにしましょう。

ViewModelFactory を開いて、コンストラクタ メソッドの内容を次のコードに差し替えてください。

//...
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
}

次に、TaskDetailViewModel を開きます。ShortcutsRepository モジュールをインポートして、同モジュール用のインスタンス変数を宣言しましょう。コードは次のようになります。

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() {
...
}

最後に、deleteTask() 関数に変更を加え、タスクが削除されるたびに、そのタスクの taskId に対応する ID のショートカットを削除するため shortcutsRepository を呼び出すようにします。

TaskDetailViewModel.kt

fun deleteTask() = viewModelScope.launch {
   _taskId.value?.let {
       //...
       shortcutsRepository.removeShortcutsById(listOf(it))
   }
}

コードをテストするには、アプリを再起動して以下を行います。

  1. テスト用タスクを削除します。
  2. 既存のタスクアイテムのタイトルを「Finish Codelab」に変更します。
  3. 「OK Google, my shortcuts」と音声で指示して Google アシスタントを開きます。
  4. 使い方・ヒント」タブをタップします。テスト用ショートカットが表示されなくなっていることを確認します。

7. 次のステップ

おつかれさまでした!ここまでに行った作業により、サンプルアプリのユーザーは Google アシスタントに「OK Google, ExampleApp で買い物リストを開いて」などの音声指示をするだけで、以前作成したメモを確認できるようになりました。ショートカットを用意することで、ユーザーはアプリ内でよく使用するアクションを手軽に実行できるようになり、より踏み込んだユーザー エンゲージメントが促進されます。

学習した内容

この Codelab では、以下の方法を学びました。

  • アプリで動的ショートカットのプッシュが必要になるユースケースを見極める
  • 各種デザイン パターン(リポジトリ、依存関係の追加、サービス ロケータ)を使ってコードの複雑さを軽減する
  • 音声での操作に対応した動的ショートカットをユーザーが作成したアプリ内コンテンツへプッシュする
  • 既存のショートカットの更新と削除を行う

この後は

サンプルのタスクリスト アプリをさらに改良してみるのもおすすめです。完成状態のプロジェクトは、GitHub の –codelab-complete branch リポジトリから参照できます。

App Actions を使ったこのアプリの拡張についてさらに理解を深めるには、以下がおすすめです。

Actions on Google の世界をさらに深く理解したい方には、以下のリソースがおすすめです。

Twitter アカウント @ActionsOnGoogle で最新情報を発信していますので、よろしければフォローをお願いします。App Actions を使って作ったものは、ぜひハッシュタグ #appActions を付けてシェアしてみてください。

フィードバック アンケート

最後に、こちらのアンケートより、この Codelab についてのフィードバックをお聞かせいただければ幸いです。