1. 始める前に
ディープリンク、ウェブリンク、Android アプリリンクについてのおさらい
ユーザーがディープリンクをたどる主な目的は、見たいコンテンツにたどり着くことです。ディープリンクには、この目的を達成するための機能がすべて備わっています。Android で処理されるリンクのタイプは次のとおりです。
- ディープリンク: ユーザーをアプリ内の特定のコンテンツに誘導するスキームの URI。
- ウェブリンク: HTTP または HTTPS スキームを使用するディープリンク。
- Android アプリリンク:
android:autoVerify
属性を含む HTTP または HTTPS スキームを使用するウェブリンク。
ディープリンク、ウェブリンク、Android アプリリンクについて詳しくは、Android ドキュメント、および YouTube と Medium の集中コースをご参照ください。
Android アプリリンクについてよくご存じの場合
技術的な詳細をすべて把握している方は、数ステップで設定して実装を完了できる方法を解説したこちらのブログ投稿をご確認ください。
Codelab の目標
この Codelab では、アプリに Android アプリリンクを構成、実装し、検証する手順のベスト プラクティスを説明します。
Android アプリリンクのメリットのひとつは安全であることです。つまり、不正なアプリが Android アプリリンクを処理することはできません。Android OS は、Android アプリリンクであることを認定するために、アプリ デベロッパーが所有するウェブサイトとリンクを必ず検証します。このプロセスを、ウェブサイトの関連付けといいます。
この Codelab の主な対象者は、ウェブサイトを所有している Android アプリ デベロッパーです。Android アプリリンクを使用すると、アプリとウェブサイト間のシームレスな連携が可能になり、ユーザー エクスペリエンスを向上させることができます。
前提条件
- adb アクティビティ マネージャーと adb パッケージ マネージャーに関する基本的な知識。
- Jetpack Compose での Android 開発およびナビゲーションに関する基本的な知識。
学習内容
- Android アプリリンク用の URL を設計する際のベスト プラクティスを学ぶ。
- Android アプリですべてのタイプのディープリンクを構成する。
- パスのワイルドカード(
path
、pathPrefix
、pathPattern
、pathAdvancePattern
)を理解する。 - Android アプリリンクの検証プロセス(Google Digital Asset Links(DAL)ファイルのアップロード、Android アプリリンクの手動検証プロセス、Play Console のディープリンク ダッシュボードを含む)について学ぶ。
- 異なる場所にあるさまざまなレストランを表示する Android アプリを作成する。
必要なもの
- Android Studio Dolphin(2021.3.1)以降。
- Google Digital Asset Link(DAL)ファイルをホストするためのドメイン(ドメインを数分で準備する方法について、こちらのブログ投稿で解説しています)。
- Google Play Console デベロッパー アカウント(任意)。Android アプリリンク構成のデバッグを別のアプローチで行うことができます。
2. コードをセットアップする
空の Compose アプリを作成する
Compose プロジェクトを開始するには、次の手順を行います。
- Android Studio で、[File] > [New] > [New Project] を選択します。
- 利用可能なテンプレートの中から [Empty Compose Activity] を選択します。
- [Next] をクリックし、プロジェクトを構成して、「Deep Links Basics」という名前を付けます。[Minimum SDK] で、必ず API レベル 21 以上(Compose がサポートしている最小 API レベル)を選択します。
- [Finish] をクリックし、プロジェクトが生成されるまで待ちます。
- アプリを起動し、アプリが動作することを確認します。空白の画面に「Hello Android!」のメッセージが表示されるはずです。
Codelab の解答
この Codelab の解答コードは GitHub から入手できます。
git clone https://github.com/android/deep-links
または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
まず、deep-links-introduction ディレクトリに移動します。solution ディレクトリ内にこのアプリがあります。自分のペースで順を追って Codelab の学習を進め、必要と思われるときに解答を確認することをおすすめします。Codelab の途中には、プロジェクトに追加する必要があるコード スニペットを記載しています。
3. ディープリンク指向の URL 設計を確認する
RESTful API 設計
リンクはウェブ開発に不可欠な要素であり、無数の反復を通じてリンク開発が行われてきた結果、さまざまな標準が確立されています。ウェブ開発におけるリンク設計の標準に注目し、応用してみることで、それらの標準をより使いやすく、維持しやすいものにできます。
こうした標準のひとつが REST(Representational State Transfer)です。REST は、一般にウェブサービス向け API を構築するために使用されるアーキテクチャとなっています。OpenAPI は、REST API を標準化するための仕様です。ディープリンク向けの URL の設計でも REST を使用できます。
なお、このセクションではウェブサービスを作成するわけではなく、URL の設計のみを取り上げます。
URL を設計する
最初に、ウェブサイトに作成される URL を確認し、それぞれが Android アプリでは何を表すかを理解しましょう。
/restaurants
- あなたが管理しているすべてのレストランを表示します。/restaurants/:restaurantName
- 特定のレストランの詳細を表示します。/restaurants/:restaurantName/orders
- レストランの注文を表示します。/restaurants/:restaurantName/orders/:orderNumber
- レストランの特定の注文を表示します。/restaurants/:restaurantName/orders/latest
- レストランの最新の注文を表示します。
URL の設計が重要な理由
Android には、別のアプリ コンポーネントからのアクションを処理するとともに、URL 検出用にも使用されるインテント フィルタがあります。特定の URL を検出する目的でインテント フィルタを定義する場合、パス プレフィックスと単純なワイルドカードに依存する構造に従う必要があります。たとえば、レストランのウェブサイトの既存 URL を基にした次の構造を見てみましょう。
https://example.com/pawtato-3140-Skinner-Hollow-Road
この URL はレストランとその場所を指定していますが、この URL を検出するインテント フィルタを Android で定義した場合に問題が生じる可能性があります。対応する Android アプリは、次のようなさまざまなレストランの URL に基づいているためです。
https://example.com/rawrbucha-2064-carriage-lane
https://example.com/pizzabus-1447-davis-avenue
これらの URL を検出するインテント フィルタをパスとワイルドカードを使って定義する場合、https://example.com/*
のようなものになります。この URL は基本的に正しく機能するものの、問題を完全に解決できたとはいえません。次に示すように、ウェブサイトの各セクションに対応する他のパスが存在するためです。
配達: https://example.com/deliveries
管理: https://example.com/admin
こうした一部の内部向け URL を Android に検出されないようにしたくても、定義したインテント フィルタ https://example.com/*
は、存在しない URL を含め、すべての URL を検出することになります。また、いずれかの URL をユーザーがクリックしたときに、ブラウザが開く(Android 12 以降)か、場合によっては確認ダイアログが表示されます(Android 12 より前)。どちらも、この設計で想定している動作ではありません。
Android では、この問題を解決するパス プレフィックスを提供しています。ただし、次のような URL の再設計が必要です。
https://example.com/*
を、次のように変更します。
https://example.com/restaurants/*
ネスト構造を追加することで、インテント フィルタが明確に定義され、デベロッパーが意図した URL を Android に検出させることができます。
URL の設計に関するベスト プラクティス
OpenAPI から集めた、ディープリンクにも適用できるおすすめの方法をいくつか紹介します。
- 公開するビジネス エンティティを中心に URL を設計します。たとえば、e コマースの場合は「顧客」と「注文」、旅行会社の場合は「チケット」と「フライト」などです。レストランのアプリとウェブサイトでは、「レストラン」と「注文」を使用します。
- ほとんどの HTTP メソッド(GET、POST、DELETE、PUT)は、発行されるリクエストを記述する動詞ですが、URL のエンドポイントに動詞を使うと紛らわしくなります。
- コレクションを記述するには、エンティティを複数形にします(例:
/restaurants/:restaurantName
)。これにより、URL が理解しやすくなり、管理も簡単になります。各 HTTP メソッドの例を以下に示します。
GET /restaurants/pawtato
POST /restaurants
DELETE /restaurants
PUT /restaurants/pawtato
どの URL も、記述の解釈と理解がしやすくなっています。なお、この Codelab では、ウェブサービス API の設計や各メソッドの詳細については説明しません。
- 関連情報を含む URL をグループ化するには、論理ネストを使用します。たとえば、いずれかのレストランの URL に、対応中の注文数を追加することが可能です。
/restaurants/1/orders
4. data 要素を確認する
AndroidManifest.xml
ファイルは Android に不可欠な要素です。アプリに関する情報を Android ビルドツール、Android OS、Google Play に提供するのが、このファイルです。
ディープリンクでは、メインの 3 つのタグ(<action>
、<category>
、<data>
)を使用してインテント フィルタを定義する必要があります。このセクションでは <data>
タグについて説明します。
<data>
要素は、リンクがユーザーによりクリックされると、そのリンクの URL 構造を Android OS に知らせます。インテント フィルタで使用できる URL の形式と構造は次のとおりです。
<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>|<pathAdvancedPattern>|<pathSuffix>]
Android は、インテント フィルタ内のすべての <data>
要素を読み取り、解析して結合し、属性のあらゆるバリエーションを解決します。次に例を示します。
AndroidManifest.xml
<intent-filter>
...
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="example.com" />
<data android:path="/restaurants" />
<data android:pathPrefix="/restaurants/orders" />
</intent-filter>
Android は以下の URL を検出します。
http://example.com/restaurants
https://example.com/restaurants
http://example.com/restaurants/orders/*
https://example.com/restaurants/orders/*
パス属性
path
(API 1 で利用可能)
この属性は、インテントの完全パスに対してマッチングされる、/
で始まる完全パスを指定します。たとえば、android:path="/restaurants/pawtato"
が一致するのは /restaurants/pawtato
のウェブサイト パスのみです。/restaurant/pawtato
の場合は s
がないため、この URL は一致しません。
pathPrefix
(API 1 で利用可能)
この属性は、インテントのパスの最初の部分のみに対してマッチングされる部分パスを指定します。たとえば、
android:pathPrefix="/restaurants"
は、レストランのパス /restaurants/pawtato
、/restaurants/pizzabus
などに一致します。
pathSuffix
(API 31 で利用可能)
この属性は、インテントのパスの末尾部分に対して完全にマッチングされるパスを指定します。たとえば、
android:pathSuffix="tato"
は、「tato」で終わるレストランのすべてのパス(/restaurants/pawtato
、/restaurants/corgtato
など)に一致します。
pathPattern
(API 1 で利用可能)
この属性は、インテントのワイルドカードを含む完全パスに対してマッチングされる完全パスを指定します。
- アスタリスク(
*
)は、直前の文字が 0 個以上出現するシーケンスに一致します。 - ピリオドとそれに続くアスタリスク(
.*
)は、0 個以上の文字のシーケンスに一致します。
例:
/restaurants/piz*abus
: このパターンは「pizzabus」レストランと一致しますが、「z
」の文字が名前に 0 個以上出現するレストラン(/restaurants/pizzabus
、/restaurants/pizzzabus
、/restaurants/pizabus
など)にも一致します。/restaurants/.*
: このパターンは/restaurants
パスが含まれるすべてのレストラン名(/restaurants/pizzabus
、/restaurants/pawtato
など)に加えて、アプリに登録されていない/restaurants/wateriehall
などにも一致します。
pathAdvancePattern
(API 31 で利用可能)
この属性は、正規表現のようなパターンを含む完全パスに対してマッチングされる完全パスを指定します。
- ピリオド(「
.
」)は任意の 1 文字に一致します。 - 角かっこ(
[...]
)は特定の文字の範囲に一致します。この集合は否定修飾子(^
)もサポートします。 - アスタリスク(
*
)は、直前のパターンの 0 回以上の繰り返しに一致します。 - プラス記号(
+
)は、直前のパターンの 1 回以上の繰り返しに一致します。 - 波かっこ(
{...}
)は、パターンが一致する回数を表します。
この属性は pathPattern
の拡張機能と考えることができ、一致する URL をより柔軟に指定することが可能です。次に例を示します。
/restaurants/[a-zA-Z]*/orders/[0-9]{3}
は、件数が最大で 3 桁のレストランの注文に一致します。/restaurants/[a-zA-Z]*/orders/latest
は、アプリに登録された全レストランの中で最新の注文に一致します。
5. ディープリンクとウェブリンクを作成する
カスタム スキームのディープリンク
カスタム スキームを使用するディープリンクは、最も一般的なタイプのディープリンクで実装が最も簡単である一方、デメリットもあります。つまり、このリンクはウェブサイトでは開けません。アプリについては、マニフェストでカスタム スキームのサポートが宣言されていれば、リンクを開くことができます。
<data>
要素では任意のスキームを使用できます。たとえば、この Codelab では food://restaurants/keybabs
という URL を使用しています。
- Android Studio で、マニフェスト ファイルに以下のインテント フィルタを追加します。
AndroidManifest.xml
<activity ... >
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="food"/>
<data android:path="/restaurants/keybabs"/>
</intent-filter>
</activity>
- カスタム スキームが含まれているリンクをアプリで開けるかどうか確認するため、メイン アクティビティに以下のコードを追加して、ホーム画面に出力します。
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Receive the intent action and data
val action: String? = intent?.action;
val data: Uri? = intent?.data;
setContent {
DeepLinksBasicsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
// Add a Column to print a message per line
Column {
// Print it on the home screen
Greeting("Android")
Text(text = "Action: $action")
Text(text = "Data: $data")
}
}
}
}
}
}
- インテントが受け取られるかどうかを調べるため、Android Debug Bridge(adb)で次のコマンドを入力します。
adb shell am start -W -a android.intent.action.VIEW -d "food://restaurants/keybabs"
このコマンドは、VIEW アクションでインテントを起動し、指定された URL をデータとして使用します。このコマンドを実行すると、アプリが起動してインテントを受け取ります。メイン画面のテキスト セクションが変化していることに注目してください。1 行目に「Hello Android!」のメッセージ、2 行目にインテントを呼び出したアクション、3 行目にはインテントを呼び出した URL が表示されています。
次の画像では、前述の adb
コマンドが実行されたことが Android Studio の下部で確認できます。右側では、アプリのホーム画面にインテントの情報が表示されています(インテントが受け取られたことを意味します)。
ウェブリンク
ウェブリンクは、カスタム スキームの代わりに http
または https
を使用するディープリンクです。
ウェブリンクの実装では、レストランが受け取った最新の注文を表す /restaurants/keybabs/order/latest.html
パスを使用します。
- 既存のインテント フィルタを使用してマニフェスト ファイルを調整します。
AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="food"/>
<data android:path="/restaurants/keybabs"/>
<!-- Web link configuration -->
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="sabs-deeplinks-test.web.app"/>
<data android:path="/restaurants/keybabs/order/latest.html"/>
</intent-filter>
両方に共通の要素(/restaurants/keybabs
)があるので、この 2 つのパスを同じインテント フィルタに含めることをおすすめします。実装がよりシンプルになり、マニフェスト ファイルも解釈しやすくなるためです。
- ウェブリンクをテストする前に、アプリを再起動して新たな変更を適用します。
- 同じ adb コマンドを使用してインテントを起動します。ただしこの場合、URL を変更します。
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/keybabs/orders/latest.html"
このスクリーンショットでは、インテントが受け取られて、ウェブブラウザが開いてウェブサイトが表示されていることに注目してください(Android 12 以降のバージョンの機能)。
6. Android アプリリンクを構成する
Android アプリリンクは、最もシームレスなユーザー エクスペリエンスを提供します。ユーザーがこのリンクをクリックすると、確認ダイアログが表示されることなく確実にアプリに遷移するためです。Android 6.0 で実装された Android アプリリンクは、最も具体的なディープリンクです。http/https
スキームと android:autoVerify
属性を使用するウェブリンクであり、一致するリンクのデフォルト ハンドラとしてアプリを指定できます。Android アプリリンクを実装するための主要な手順は、次の 2 つです。
- 適切なインテント フィルタを使用してマニフェスト ファイルを更新する。
- 検証用のウェブサイトの関連付けを追加する。
マニフェスト ファイルを更新する
- Android アプリリンクをサポートするには、マニフェスト ファイルで、以前の構成を次の内容に置き換えます。
AndroidManifest.xml
<!-- Replace deep link and web link configuration with this -->
<!-- Please update the host with your own domain -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"/>
<data android:host="example.com"/>
<data android:pathPrefix="/restaurants"/>
</intent-filter>
このインテント フィルタでは、android:autoVerify
属性を追加し、値を true に設定しています。これにより、アプリがインストールされるとき、および更新されるたびに、Android OS がドメインの所有権を確認できるようになります。
ウェブサイトの関連付け
Android アプリリンクを検証するために、アプリとウェブサイト間の関連付けを作成します。検証が発生するには、ウェブサイトで Google Digital Asset Links(DAL)JSON ファイルを公開する必要があります。
Google DAL は、他のアプリやウェブサイトに関する検証可能なステートメントを定義するプロトコルと API です。この Codelab では、assetlinks.json
ファイルに、Android アプリに関するステートメントを作成します。次に例を示します。
assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.devrel.deeplinksbasics",
"sha256_cert_fingerprints":
["B0:4E:29:05:4E:AB:44:C6:9A:CB:D5:89:A3:A8:1C:FF:09:6B:45:00:C5:FD:D1:3E:3E:12:C5:F3:FB:BD:BA:D3"]
}
}]
このファイルには複数のステートメントを格納できますが、この例では 1 つのアイテムのみ示されています。各ステートメントには必ず以下のフィールドを含める必要があります。
- Relation: target に関して宣言される 1 つ以上の関係を記述します。
- Target: このステートメントが適用されるアセット。利用可能な 2 つのターゲット(
web
またはandroid_app
)のいずれかを指定できます。
Android ステートメントの target
プロパティには、次のフィールドが含まれます。
namespace
: すべての Android アプリでandroid_app
を指定します。package_name
: 完全修飾されたパッケージ名(com.devrel.deeplinksbasics
)。sha256_cert_fingerprints
: アプリの証明書のフィンガープリント。この証明書を生成する方法については、次のセクションで学習します。
証明書フィンガープリント
証明書フィンガープリントを取得する方法はいくつかあります。この Codelab では 2 種類の方法を使用します。一方はアプリのデバッグビルドに対して行うもの、もう一方はアプリを Google Play ストアにリリースする際に役立つものです。
デバッグ構成
作成したプロジェクトを Android Studio で初めて実行すると、デバッグ用証明書で自動的にアプリが署名されます。この証明書の場所は $HOME/.android/debug.keystore
です。この SHA-256 証明書フィンガープリントは Gradle コマンドを使用して取得できます。以下がその手順です。
Control
キーを 2 回押します。[Run anything] メニューが表示されます。表示されない場合は、右側サイドバーの Gradle メニューを開いてから、Gradle アイコンをクリックします。
- 「
gradle signingReport
」と入力してEnter
キーを押します。コンソールでコマンドが実行され、デバッグアプリ バリアントのフィンガープリント情報が表示されます。
- ウェブサイトの関連付けを完了するには、SHA-256 証明書フィンガープリントをコピーして、JSON ファイルを更新し、これをウェブサイトの
https://<domain>/.well-know/assetlinks.json
の場所にアップロードします。設定方法については、Android アプリリンクのブログ投稿をご覧ください。 - アプリがそのまま実行される場合は、[Stop] を押してアプリを停止します。
- 検証プロセスを再起動するには、アプリをシミュレータから削除します。シミュレータで DeepLinksBasics アプリアイコンを長押しして、[App Info] を選択します。[Uninstall] をクリックしてモーダルで確定します。その後アプリを実行すると、Android Studio により関連付けが検証されます。
- 実行構成として [app] が選択されていることを確認します。選択されていないと、Gradle 署名レポートが再度実行されます。
- アプリを再起動し、Android アプリリンク URL を指定してインテントを起動します。
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/"
- アプリが起動して、インテントがホーム画面に表示されるようになります。
以上で、初めての Android アプリリンクの作成が完了しました。
リリース構成
Android アプリリンクを実装したアプリを Google Play ストアにアップロードできるようにするためには、適切な証明書フィンガープリントがあるリリースビルドを使用する必要があります。このビルドを生成してアップロードする手順は次のとおりです。
- Android Studio のメインメニューで、[Build] > [Generate Signed Bundle/APK] をクリックします。
- 次のダイアログで、[Android App Bundle](Play アプリ署名を使用する場合)または [APK](デバイスに直接デプロイする場合)を選択します。
- 次のダイアログで、[Key store path] の下にある [Create new] をクリックします。新しいウィンドウが開きます。
- キーストアのパスを選択し、「
basics-keystore.jks
」という名前を付けます。 - キーストアのパスワードを作成し、確認します。
- キーのエイリアスはデフォルト値のままにします。
- キーのパスワード(および確認用パスワード)には、キーストアと同じものを入力します。両方が一致する必要があります。
- [Certificate] の情報を入力し、[OK] をクリックします。
- Play アプリ署名用に暗号化された鍵をエクスポートするオプションをオンにして、[Next] をクリックします。
- このダイアログで、リリースビルド バリアントを選択し、[Finish] をクリックします。これで、Google Play ストアにアプリをアップロードできます。また、Play アプリ署名も使用できるようになりました。
Play アプリ署名
Play アプリ署名を使用すると、Google が提供するサービスを通じてアプリの署名鍵を管理および保護できます。デベロッパーが行う必要があるのは、アプリの署名付きバンドル(前のステップで生成されたもの)をアップロードすることだけです。
assetlinks.json
ファイルの証明書フィンガープリントを取得してリリースビルド バリアントで Android アプリリンクを使用するには、次の手順を行います。
- Google Play Console で [アプリを作成] をクリックします。
- [アプリ名] に「Deep Links Basics」と入力します。
- 次の 2 つの設定では [アプリ] と [無料] を選択します。
- [申告] に同意し、[アプリを作成] をクリックします。
- バンドルをアップロードして、Android アプリリンクをテストできるようにするには、左側のメニューで [テスト] > [内部テスト] を選択します。
- [新しいリリースを作成] をクリックします。
- 次の画面で、[アップロード] をクリックしてから、前のセクションで生成されたバンドルを選択します。
app-release.aab
ファイルは、[DeepLinksBascis] > [app] > [release] 内で見つかります。[開く] をクリックして、バンドルがアップロードされるまで待ちます。 - アップロードが完了したら、残りのフィールドはデフォルト値のままにします。[保存] をクリックします。
- 次のセクションの準備をするために、[リリースのレビュー] をクリックしてから、次の画面で [内部テストとしての公開を開始] をクリックします。この Codelab では Google Play ストアへの公開は行わないので、警告メッセージは無視します。
- モーダルで [公開] をクリックします。
- Play アプリ署名により作成された SHA-256 証明書フィンガープリントを取得するには、左側のメニューで [ディープリンク] タブをクリックし、ディープリンク ダッシュボードを表示します。
- [ドメイン] セクションで、ウェブサイトのドメインをクリックします。アプリではドメインが検証されていない(ウェブサイトの関連付けがない)という情報が表示されます。
- [ドメインの問題を解決してください] セクションで [さらに表示] の矢印をクリックします。
- この画面には、証明書フィンガープリントを追加して
assetlinks.json
ファイルを更新する方法が示されます。コード スニペットをコピーして、assetlinks.json
ファイルを更新します。
assetlinks.json
ファイルが更新されたら、[もう一度確認する] をクリックします。所有権の確認が完了しない場合は、所有権確認サービスにより新しい変更が検出されるまで、5 分ほど待ちます。- ディープリンク ダッシュボード ページを再読み込みすると、所有権確認エラーがない状態になります。
アップロードされたアプリの検証
シミュレータで実行されるアプリを検証する方法はすでに確認しました。ここでは、Google Play ストアにアップロードされたアプリを検証します。
エミュレータにアプリをインストールして Android アプリリンクを検証できるようにするには、次の手順を行います。
- 左側のサイドバーで [リリースの概要] をクリックしてから、アップロードした最新リリース(1(1.0)リリース)を選択します。
- [リリースの詳細](右側の青い矢印)をクリックして、リリースの詳細を確認します。
- 同じく右側の青い矢印をクリックして、App Bundle の情報を確認します。
- このモーダルで [ダウンロード] タブを選択し、[署名済みのユニバーサル APK] の [ダウンロード] をクリックします。
- このバンドルをシミュレータにインストールする前に、Android Studio によりインストールされた前のアプリを削除します。
- シミュレータで、DeepLinksBasics アプリアイコンを長押しし、[App Info] を選択します。[Uninstall] をクリックしてモーダルで確定します。
- ダウンロードしたバンドルをインストールするには、ダウンロードした
1.apk
ファイルをシミュレータ画面にドラッグ&ドロップして、インストールが完了するまで待ちます。
- 検証用のテストを行うには、Android Studio でターミナルを開き、次の 2 つのコマンドを使用して検証プロセスを実行します。
adb shell pm verify-app-links --re-verify com.devrel.deeplinksbasics
adb shell pm get-app-links com.devrel.deeplinksbasics
get-app-links
コマンドを実行すると、コンソールに「verified
」というメッセージが表示されます。「legacy_failure
」というメッセージが表示された場合は、証明書フィンガープリントが、ウェブサイト用にアップロードしたものと一致しているかどうか確認してください。一致しているにもかからわず「verified」のメッセージが表示されない場合は、ステップ 6、7、8 をもう一度試してみてください。
7. Android アプリリンクを実装する
すべての構成が完了したので、続いてアプリを実装しましょう。
実装には Jetpack Compose を使用します。Jetpack Compose の詳細については、Jetpack Compose でより優れたアプリを迅速にビルドするをご覧ください。
コードの依存関係
このプロジェクトに必要な依存関係を追加して更新するには、次の手順を行います。
Module
とProject
の Gradle ファイルに次のコードを追加します。
build.gradle(プロジェクト)
buildscript {
...
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:2.43"
}
}
build.gradle(モジュール)
plugins {
...
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
...
dependencies {
...
implementation 'androidx.compose.material:material:1.2.1'
...
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
implementation "com.google.dagger:hilt-android:2.43"
kapt "com.google.dagger:hilt-compiler:2.43"
}
プロジェクトの ZIP ファイルには画像のディレクトリがあり、それぞれのレストランに使用可能なロイヤリティフリーの画像が 10 個保存されています。これらの画像は自由にお使いいただけます。また、独自の画像を追加してもかまいません。
HiltAndroidApp
のメインのエントリ ポイントを追加するには、次の手順を行います。
- 「
DeepLinksBasicsApplication.kt
」という名前の新しい Kotlin クラス / ファイルを作成し、この新しいアプリ名を指定してマニフェスト ファイルを更新します。
DeepLinksBasicsApplication.kt
package com.devrel.deeplinksbasics
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class DeepLinksBasicsApplication : Application() {}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Update name property -->
<application
android:name=".DeepLinksBasicsApplication"
...
データ
Restaurant
のクラス、リポジトリ、ローカル データソースを指定した、レストラン用のデータレイヤを作成する必要があります。作成が必要な data
パッケージ内にすべてが格納されます。手順は次のとおりです。
Restaurant.kt
ファイルで、次のコード スニペットを追加してRestaurant
クラスを作成します。
Restaurant.kt
package com.devrel.deeplinksbasics.data
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
@Immutable
data class Restaurant(
val id: Int = -1,
val name: String = "",
val address: String = "",
val type: String = "",
val website: String = "",
@DrawableRes val drawable: Int = -1
)
RestaurantLocalDataSource.kt
ファイルで、データソース クラスにレストランをいくつか追加します。データをカスタム ドメインに置き換えるようにしてください。次のコード スニペットを使用できます。
RestaurantLocalDataSource.kt
package com.devrel.deeplinksbasics.data
import com.devrel.deeplinksbasics.R
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RestaurantLocalDataSource @Inject constructor() {
val restaurantList = listOf(
Restaurant(
id = 1,
name = "Pawtato",
address = "3140 Skinner Hollow Road, Medford, Oregon 97501",
type = "Potato and gnochi",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/pawtato/",
drawable = R.drawable.restaurant1,
),
Restaurant(
id = 2,
name = "Rawrbucha",
address = "2064 Carriage Lane, Mansfield, Ohio 44907",
type = "Kombucha",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/rawrbucha/",
drawable = R.drawable.restaurant2,
),
Restaurant(
id = 3,
name = "Pizzabus",
address = "1447 Davis Avenue, Petaluma, California 94952",
type = "Pizza",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/pizzabus/",
drawable = R.drawable.restaurant3,
),
Restaurant(
id = 4,
name = "Keybabs",
address = "3708 Pinnickinnick Street, Perth Amboy, New Jersey 08861",
type = "Kebabs",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/keybabs/",
drawable = R.drawable.restaurant4,
),
Restaurant(
id = 5,
name = "BBQ",
address = "998 Newton Street, Saint Cloud, Minnesota 56301",
type = "BBQ",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/bbq/",
drawable = R.drawable.restaurant5,
),
Restaurant(
id = 6,
name = "Salades",
address = "4522 Rockford Mountain Lane, Oshkosh, Wisconsin 54901",
type = "salads",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/salades/",
drawable = R.drawable.restaurant6,
),
Restaurant(
id = 7,
name = "Gyros and moar",
address = "1993 Bird Spring Lane, Houston, Texas 77077",
type = "Gyro",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/gyrosAndMoar/",
drawable = R.drawable.restaurant7,
),
Restaurant(
id = 8,
name = "Peruvian ceviche",
address = "2125 Deer Ridge Drive, Newark, New Jersey 07102",
type = "seafood",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/peruvianCeviche/",
drawable = R.drawable.restaurant8,
),
Restaurant(
id = 9,
name = "Vegan burgers",
address = "594 Warner Street, Casper, Wyoming 82601",
type = "vegan",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/veganBurgers/",
drawable = R.drawable.restaurant9,
),
Restaurant(
id = 10,
name = "Taquitos",
address = "1654 Hart Country Lane, Blue Ridge, Georgia 30513",
type = "mexican",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/taquitos/",
drawable = R.drawable.restaurant10,
),
)
}
- 画像をプロジェクトにインポートします。
- 次に、
RestaurantRepository.kt
ファイルで、レストランの名前を取得する関数を指定したRestaurant
リポジトリを追加します。コード スニペットは次のとおりです。
RestaurantRepository.kt
package com.devrel.deeplinksbasics.data
import javax.inject.Inject
class RestaurantRepository @Inject constructor(
private val restaurantLocalDataSource: RestaurantLocalDataSource
){
val restaurants: List<Restaurant> = restaurantLocalDataSource.restaurantList
// Method to obtain a restaurant object by its name
fun getRestaurantByName(name: String): Restaurant ? {
return restaurantLocalDataSource.restaurantList.find {
val processedName = it.name.filterNot { it.isWhitespace() }.lowercase()
val nameToTest = name.filterNot { it.isWhitespace() }.lowercase()
nameToTest == processedName
}
}
}
ViewModel
アプリで Android アプリリンクからレストランを選択できるようにするには、選択中のレストランの値を変更する ViewModel
を作成する必要があります。手順は次のとおりです。
RestaurantViewModel.kt
ファイルに次のコード スニペットを追加します。
RestaurantViewModel.kt
package com.devrel.deeplinksbasics.ui.restaurant
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.devrel.deeplinksbasics.data.Restaurant
import com.devrel.deeplinksbasics.data.RestaurantRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RestaurantViewModel @Inject constructor(
private val restaurantRepository: RestaurantRepository,
) : ViewModel() {
// restaurants and selected restaurant could be used as one UIState stream
// which will scale better when exposing more data.
// Since there are only these two, it is okay to expose them as separate streams
val restaurants: List<Restaurant> = restaurantRepository.restaurants
private val _selectedRestaurant = MutableStateFlow<Restaurant?>(value = null)
val selectedRestaurant: StateFlow<Restaurant?>
get() = _selectedRestaurant
// Method to update the current restaurant selection
fun updateSelectedRestaurantByName(name: String) {
viewModelScope.launch {
val selectedRestaurant: Restaurant? = restaurantRepository.getRestaurantByName(name)
if (selectedRestaurant != null) {
_selectedRestaurant.value = selectedRestaurant
}
}
}
}
Compose
ViewModel とデータレイヤのロジックを作成したら、次は UI レイヤを追加します。Jetpack Compose ライブラリを使用すれば数ステップで完了します。このアプリでは、レストランをカードのグリッドで表示します。カードをクリックしたユーザーが、そのレストランの詳細情報に移動できるようにします。コンポーズ可能なメインの関数が 3 つ、対応するレストランに誘導するナビゲーション コンポーネントが 1 つ必要です。
UI レイヤを追加する手順は次のとおりです。
- まず、各レストランの詳細情報を表示するコンポーズ可能な関数を指定します。
RestaurantCardDetails.kt
ファイルに次のコード スニペットを追加します。
RestaurantCardDetails.kt
package com.devrel.deeplinksbasics.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant
@Composable
fun RestaurantCardDetails (
restaurant: Restaurant,
onBack: () -> Unit,
) {
BackHandler() {
onBack()
}
Scaffold(
topBar = {
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
) {
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.padding(start = 8.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Arrow Back",
modifier = Modifier.clickable {
onBack()
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = restaurant.name)
}
}
}
) { paddingValues ->
Card(
modifier = Modifier
.padding(paddingValues)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(8.dp))
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = restaurant.name, style = MaterialTheme.typography.h6)
Text(text = restaurant.type, style = MaterialTheme.typography.caption)
Text(text = restaurant.address, style = MaterialTheme.typography.caption)
SelectionContainer {
Text(text = restaurant.website, style = MaterialTheme.typography.caption)
}
Image(painter = painterResource(id = restaurant.drawable), contentDescription = "${restaurant.name}")
}
}
}
}
- 次に、グリッドセルとグリッド自体を実装します。
RastaurantCell.kt
ファイルに次のコード スニペットを追加します。
RestaurantCell.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant
@Composable
fun RestaurantCell(
restaurant: Restaurant
){
Card(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 8.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(8.dp))
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = restaurant.name, style = MaterialTheme.typography.h6)
Text(text = restaurant.address, style = MaterialTheme.typography.caption)
Image(painter = painterResource(id = restaurant.drawable), contentDescription = "${restaurant.name}")
}
}
}
RestaurantGrid.kt
ファイルに次のコード スニペットを追加します。
RestaurantGrid.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant
@Composable
fun RestaurantGrid(
restaurants: List<Restaurant>,
onRestaurantSelected: (String) -> Unit,
navigateToRestaurant: (String) -> Unit,
) {
Scaffold(topBar = {
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
) {
Text(text = "Restaurants", fontWeight = FontWeight.Bold)
}
}) { paddingValues ->
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 200.dp),
modifier = Modifier.padding(paddingValues)
) {
items(items = restaurants) { restaurant ->
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
onRestaurantSelected(restaurant.name)
navigateToRestaurant(restaurant.name)
})
) {
RestaurantCell(restaurant)
}
}
}
}
}
- 次に、アプリの状態とナビゲーション ロジックを実装して、
MainActivity.kt
を更新する必要があります。これにより、ユーザーがレストラン カードをクリックしたときに特定のレストランに誘導できるようになります。RestaurantAppState.kt
ファイルに次のコード スニペットを追加します。
RestaurantAppState.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
sealed class Screen(val route: String) {
object Grid : Screen("restaurants")
object Name : Screen("restaurants/{name}") {
fun createRoute(name: String) = "restaurants/$name"
}
}
@Composable
fun rememberRestaurantAppState(
navController: NavHostController = rememberNavController(),
) = remember(navController) {
RestaurantAppState(navController)
}
class RestaurantAppState(
val navController: NavHostController,
) {
fun navigateToRestaurant(restaurantName: String) {
navController.navigate(Screen.Name.createRoute(restaurantName))
}
fun navigateBack() {
navController.popBackStack()
}
}
- ナビゲーションでは、
NavHost
を作成し、各レストランに誘導するコンポーズ可能なルートを使用する必要があります。RestaurantApp.kt
ファイルに次のコード スニペットを追加します。
RestaurantApp.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.devrel.deeplinksbasics.ui.restaurant.RestaurantViewModel
@Composable
fun RestaurantApp(
viewModel: RestaurantViewModel = viewModel(),
appState: RestaurantAppState = rememberRestaurantAppState(),
) {
val selectedRestaurant by viewModel.selectedRestaurant.collectAsState()
val onRestaurantSelected: (String) -> Unit = { viewModel.updateSelectedRestaurantByName(it) }
NavHost(
navController = appState.navController,
startDestination = Screen.Grid.route,
) {
// Default route that points to the restaurant grid
composable(Screen.Grid.route) {
RestaurantGrid(
restaurants = viewModel.restaurants,
onRestaurantSelected = onRestaurantSelected,
navigateToRestaurant = { restaurantName ->
appState.navigateToRestaurant(restaurantName)
},
)
}
// Route for the navigation to a particular restaurant when a user clicks on it
composable(Screen.Name.route) {
RestaurantCardDetails(restaurant = selectedRestaurant!!, onBack = appState::navigateBack)
}
}
}
- これで、アプリ インスタンスを使用して
MainActivity.kt
を更新できるようになりました。このファイルを次のコードで置き換えます。
MainActivity .kt
package com.devrel.deeplinksbasics
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import com.devrel.deeplinksbasics.ui.RestaurantApp
import com.devrel.deeplinksbasics.ui.theme.DeepLinksBasicsTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DeepLinksBasicsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
RestaurantApp()
}
}
}
}
}
- アプリを実行し、グリッド内を移動して特定のレストランを選択します。いずれかのレストランを選択すると、そのレストランの詳細情報がアプリに表示されるはずです。
Android アプリリンク
それでは、グリッドと各レストランに Android アプリリンクを追加しましょう。グリッドの AndroidManifest.xml
セクションは、すでに /restaurants
に用意されています。必要な作業はロジックに新しいルート構成を追加することだけで、すべてのレストランに同じ設定を使用できるので、非常に効率的です。手順は次のとおりです。
/restaurants
をパスとして受け取るためのインテント フィルタを使用してマニフェスト ファイルを更新します。また、ドメインをホストとして追加します。AndroidManifest.xml
ファイルに次のコード スニペットを追加します。
AndroidManifest.xml
...
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="your.own.domain"/>
<data android:pathPrefix="/restaurants"/>
</intent-filter>
RestaurantApp.kt
ファイルに次のコード スニペットを追加します。
RestaurantApp.kt
...
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
fun RestaurantApp(...){
NavHost(...){
...
// Route for the navigation to a particular restaurant when a user clicks on it
// and for an incoming deep link
// Update with your own domain
composable(Screen.Name.route,
deepLinks = listOf(
navDeepLink { uriPattern = "https://your.own.domain/restaurants/{name}" }
),
arguments = listOf(
navArgument("name") {
type = NavType.StringType
}
)
) { entry ->
val restaurantName = entry.arguments?.getString("name")
if (restaurantName != null) {
LaunchedEffect(restaurantName) {
viewModel.updateSelectedRestaurantByName(restaurantName)
}
}
selectedRestaurant?.let {
RestaurantCardDetails(
restaurant = it,
onBack = appState::navigateBack
)
}
}
}
}
内部では、Android インテントの Uri
データとコンポーズ可能なルートのマッチングが NavHost
により実行されます。ルートが一致すると、composable
が表示されます。
composable
コンポーネントは deepLinks
パラメータを受け取ることができます。このパラメータには、インテント フィルタから受け取った URI のリストが含まれています。この Codelab では、作成済みのウェブサイトを追加し、受け取る ID パラメータを定義して、ユーザーを特定のレストランに誘導します。
- このアプリロジックにより、Android アプリリンクをクリックしたユーザーが対応するレストランに誘導されることを確認するには、
adb
を使用します。
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/gyrosAndMoar"
このように、アプリには対応するレストランが表示されます。
8. Play Console ダッシュボードを確認する
すでに見てきたとおり、ディープリンク ダッシュボードには、ディープリンクが正しく機能するために必要な情報がすべて表示されます。この情報はアプリのバージョンごとに確認することも可能です。マニフェスト ファイルに追加されたドメイン、リンク、カスタムリンクはもちろん、問題がある場合には assetlinks.json
ファイル内で更新すべき場所も示してくれます。
9. おわりに
これで、Android アプリリンクを使用するアプリを初めて構築できました。
ここで学習したのは、Android アプリリンクの設計、構成、作成、テストという一連のプロセスです。このプロセスには多数の異なる要素が含まれています。そこで、皆さんの Android OS 開発に役立つよう、この Codelab にすべての詳細情報を集約しました。
Android アプリリンクが正しく機能するための主な手順を理解していただけたことと思います。