1. 始める前に
この Codelab では、前回の Codelab で実装を開始した Cupcake アプリの残りの部分を完成させます。Cupcake アプリには複数の画面があり、カップケーキの注文フローが表示されます。完成したアプリでは、ユーザーはアプリで次の操作を行えます。
- カップケーキの注文を作成する
- 上へボタンまたは戻るボタンを使用して、注文フローの前のステップに移動する
- 注文をキャンセルする
- メールアプリなどの別のアプリに注文を送信する
実装の過程で、Android がアプリのタスクとバックスタックを処理する方法を学びます。学習を通じて、注文をキャンセルしたユーザーを(注文フローの前の画面ではなく)アプリの最初の画面に戻すなどのシナリオで、バックスタックを操作できるようになります。
前提条件
- アクティビティ内のフラグメント間で共有ビューモデルを作成し、使用できる
- Jetpack Navigation コンポーネントの使用に精通している
- UI とビューモデルの同期を維持するために LiveData とのデータ バインディングを使用した経験がある
- 新しいアクティビティを開始するインテントを作成できる
学習内容
- ナビゲーションがアプリのバックスタックに与える影響
- カスタムのバックスタック動作を実装する方法
作成するアプリの概要
- 別のアプリへの注文の送信と、注文のキャンセルができるカップケーキ注文アプリ
必要なもの
- Android Studio がインストールされているパソコン
- 前回の Codelab を完了した時点の Cupcake アプリのコード
2. スターター アプリの概要
この Codelab では、前回の Codelab で作成した Cupcake アプリを使用します。前回の Codelab を完了した時点のコードを使用するか、GitHub からスターター コードをダウンロードしてください。
この Codelab のスターター コードをダウンロードする
GitHub からスターター コードをダウンロードした場合、プロジェクトのフォルダ名は android-basics-kotlin-cupcake-app-viewmodel
になります。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開くまで待ちます。
- 実行ボタン をクリックし、アプリをビルドして実行します。正常にビルドされたことを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
アプリを実行すると、次のようになります。
この Codelab では、最初に上へボタンをアプリに実装し、ユーザーがこのボタンをタップして注文フローの前のステップに移動できるようにします。
次に、キャンセル ボタンを追加して、注文手続き中に気が変わった場合に注文をキャンセルできるようにします。
さらに、アプリを拡張して、[Send Order to Another App] をタップすると別のアプリと注文を共有できるようにします。たとえば、ケーキ店にメールで注文を送信します。
それでは、Cupcake アプリを詳しく検討して完成させましょう。
3.上へボタンの動作を実装する
Cupcake アプリでは、前の画面に戻る矢印がアプリバーに表示されます。これは、前の Codelab で学習したとおり、上へボタンと呼ばれます。現在、アプリの上へボタンは何も実行しないので、このナビゲーションのバグを修正しましょう。
MainActivity
には、ナビゲーション コントローラを使用してアプリバー(アクションバーとも呼びます)を設定するコードがすでに存在します。navController
をクラス変数にして、別のメソッドでも使用できるようにします。
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
}
- 同じクラス内に、
onSupportNavigateUp()
関数をオーバーライドするコードを追加します。このコードは、アプリ内を遡るナビゲーションを処理することをnavController
に要求します。失敗した場合は、上へボタンを処理するスーパークラス実装(AppCompatActivity
内にあります)にフォールバックします。
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
- アプリを実行します。
FlavorFragment
、PickupFragment
、SummaryFragment
から、上へボタンが機能するはずです。注文フローの前のステップに戻ると、フラグメントはビューモデルからの正しいフレーバーと受け取り日を表示します。
4. タスクとバックスタックについて学ぶ
次に、アプリの注文フローにキャンセル ボタンを導入します。注文手続きの任意の時点で注文をキャンセルすると、ユーザーは StartFragment
に戻ります。この動作を処理するため、Android のタスクとバックスタックについて学習しましょう。
タスク
Android のアクティビティはタスク内に存在します。ランチャー アイコンから初めてアプリを開くと、Android はメイン アクティビティを含む新しいタスクを作成します。「タスク」とは、ユーザーが特定のジョブ(メールの確認、カップケーキの注文の作成、写真の撮影など)を実行するときに操作するアクティビティのコレクションです。
アクティビティは、「バックスタック」と呼ばれるスタック内に配列されます。ユーザーが新しいアクティビティにアクセスするたびに、各アクティビティはタスクのバックスタックにプッシュされます。これは、積み重なったパンケーキに喩えることができます。新しいパンケーキは前のパンケーキの上に積み重ねられます。スタックの一番上にあるアクティビティは、ユーザーが現在操作しているアクティビティです。その下にあるアクティビティはバックグラウンドに移動され、停止されています。
バックスタックは、ユーザーが前の画面に戻りたいときに役立ちます。Android は、現在のアクティビティをスタックの一番上から取り除いて破棄し、その下にあったアクティビティを再開できます。言い換えると、スタックからアクティビティを「ポップ」し、前のアクティビティをフォアグラウンドに移動してユーザーが操作できるようにします。ユーザーがいくつか前の画面に戻りたい場合、Android はアクティビティをスタックの一番上からポップし続け、スタックの一番下に近づきます。バックスタックにアクティビティがなくなったら、ユーザーはデバイス(またはアクティビティを起動したアプリ)のランチャー画面に戻されます。
2 つのアクティビティ(MainActivity
と DetailActivity
)を実装した Words アプリのバージョンを見てみましょう。
アプリを初めて起動すると、MainActivity
が開いて、タスクのバックスタックに追加されます。
文字をクリックすると、DetailActivity
が起動して、バックスタックにプッシュされます。これは、DetailActivity
が作成、開始、再開され、ユーザーが操作できるようになったことを意味します。MainActivity
はバックグラウンドに移動されます。以下の図ではグレーの背景色で示されています。
戻るボタンをタップすると、DetailActivity
がバックスタックからポップされ、DetailActivity
インスタンスは破棄されて終了します。
その後、バックスタックの一番上にある次のアイテム(MainActivity
)がフォアグラウンドに移動されます。
バックスタックは、ユーザーが開始したアクティビティをトラッキングできるのと同様に、Jetpack Navigation コンポーネントを利用して、ユーザーがアクセスしたフラグメント デスティネーションをトラッキングすることもできます。
Navigation ライブラリを使用すると、ユーザーが戻るボタンをタップするたびに、バックスタックからフラグメント デスティネーションをポップできます。このデフォルトの動作は無償で提供され、実装する必要はありません。コードを記述しなければならないのは、カスタムのバックスタック動作が必要な場合だけです(Cupcake アプリではカスタムの動作が必要です)。
Cupcake アプリのデフォルトの動作
それでは、Cupcake アプリでバックスタックがどのように機能するかを見てみましょう。アプリにアクティビティは 1 つしかありませんが、ユーザーがナビゲートするフラグメント デスティネーションは複数あります。そのため、戻るボタンがタップされるたびに、直前のフラグメント デスティネーションに戻ることが求められます。
初めてアプリを開いたときは、StartFragment
デスティネーションが表示されます。このデスティネーションは、スタックの一番上にプッシュされます。
1 個のカップケーキの注文を選択すると、FlavorFragment
に移動し、このフラグメントはバックスタックにプッシュされます。
フレーバーを選択して次へをタップすると、PickupFragment
に移動し、このフラグメントはバックスタックにプッシュされます。
最後に、受け取り日を選択して次へをタップすると、SummaryFragment
に移動し、このフラグメントはバックスタックの一番上に追加されます。
SummaryFragment
から戻るボタンまたは上へボタンをタップします。すると、SummaryFragment
がスタックからポップされ、破棄されます。
PickupFragment
がバックスタックの一番上になり、ユーザーに表示されます。
戻るボタンまたは上へボタンを再度タップします。PickupFragment
がスタックからポップされ、FlavorFragment
が表示されます。
戻るボタンまたは上へボタンを再度タップします。FlavorFragment
がスタックからポップされ、StartFragment
が表示されます。
注文フローの前のステップに戻る過程では、一度に 1 つのデスティネーションのみがポップされます。しかし、次のタスク(注文をキャンセルする機能をアプリに追加します)では、ユーザーを StartFragment
に戻して新しい注文を開始するために、バックスタック内の複数のデスティネーションを一度にポップする必要があります。
Cupcake アプリのバックスタックを変更する
注文のキャンセル ボタンをユーザーに提供するため、FlavorFragment
、PickupFragment
、および SummaryFragment
クラスとレイアウト ファイルを変更します。
ナビゲーション アクションを追加する
まず、アプリのナビゲーション グラフにナビゲーション アクションを追加して、ユーザーが StartFragment
以降のデスティネーションからそこに戻れるようにします。
- res > navigation > nav_graph.xml ファイルに移動してデザインビューを選択し、Navigation Editor を開きます。
- 現在は、
startFragment
からflavorFragment
へのアクション、flavorFragment
からpickupFragment
へのアクション、pickupFragment
からsummaryFragment
へのアクションがあります。 - クリックしてドラッグし、
summaryFragment
からstartFragment
への新しいナビゲーション アクションを作成します。ナビゲーション グラフでデスティネーションを接続する方法を復習したい場合は、こちらの手順をご覧ください。 pickupFragment
からクリックしてドラッグし、startFragment
への新しいアクションを作成します。flavorFragment
からクリックしてドラッグし、startFragment
への新しいアクションを作成します。- 完了すると、ナビゲーション グラフは次のようになります。
以上の変更により、ユーザーは注文フローの途中のフラグメントから注文フローの開始点に戻れるようになります。これらのアクションを実際にナビゲートするコードが必要です。コードを挿入する適切なポイントは、キャンセル ボタンがタップされたときです。
キャンセル ボタンをレイアウトに追加する
まず、StartFragment
を除くすべてのフラグメントのレイアウト ファイルにキャンセル ボタンを追加します。すでに注文フローの最初の画面にいる場合、注文をキャンセルする必要はありません。
fragment_flavor.xml
レイアウト ファイルを開きます。- 分割ビューを使用して、XML を直接編集し、プレビューを横に並べて表示します。
- 小計のテキストビューと次へボタンの間にキャンセル ボタンを追加します。ボタンにリソース ID
@+id/cancel_button
を割り当て、@string/cancel
として表示されるテキストを含めます。
次へボタンの横にボタンを配置し、ボタンの行として表示されるようにします。縦方向の制約については、キャンセル ボタンの最上部を次へボタンの最上部に制限します。横方向の制約については、キャンセル ボタンの開始位置を親コンテナに制限し、終了位置を次へボタンの開始位置に制限します。
また、キャンセル ボタンの高さを wrap_content
にし、幅を 0dp
にして、画面の幅を他のボタンと均等に分割します。なお、次のステップが終わるまで、[Preview] パネルにボタンは表示されません。
...
<TextView
android:id="@+id/subtotal" ... />
<Button
android:id="@+id/cancel_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/cancel"
app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/next_button" />
<Button
android:id="@+id/next_button" ... />
...
- また、
fragment_flavor.xml
で、次へボタンの開始制約をapp:layout_constraintStart_toStartOf="parent
からapp:layout_constraintStart_toEndOf="@id/cancel_button"
に変更する必要があります。さらに、キャンセル ボタンに終了余白を追加して、2 つのボタンの間に空白を設けます。これで、Android Studio の [Preview] パネルにキャンセル ボタンが表示されるようになります。
...
<Button
android:id="@+id/cancel_button"
android:layout_marginEnd="@dimen/side_margin" ... />
<Button
android:id="@+id/next_button"
app:layout_constraintStart_toEndOf="@id/cancel_button"... />
...
- 視覚的スタイルについては、マテリアルの枠線付きボタンスタイル(属性
style="?attr/materialButtonOutlinedStyle"
を含む)を適用し、キャンセル ボタンが次へボタンほど目立たないようにします。ユーザーの注意を引き付けたいメイン アクションは次に進む操作であるからです。
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle" ... />
これで、ボタンの外観と配置が適切なものになりました。
- 同様に、
fragment_pickup.xml
レイアウト ファイルにキャンセル ボタンを追加します。
...
<TextView
android:id="@+id/subtotal" ... />
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/side_margin"
android:text="@string/cancel"
app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/next_button" />
<Button
android:id="@+id/next_button" ... />
...
- 次へボタンの開始制約も同様に更新します。これにより、プレビューにキャンセル ボタンが表示されます。
<Button
android:id="@+id/next_button"
app:layout_constraintStart_toEndOf="@id/cancel_button" ... />
fragment_summary.xml
ファイルにも同様の変更を適用しますが、このフラグメントのレイアウトには少し違いがあります。親の縦方向のLinearLayout
で送信ボタンの下にキャンセル ボタンを追加し、これらのボタンの間に余白を挿入します。
...
<Button
android:id="@+id/send_button" ... />
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_between_elements"
android:text="@string/cancel" />
</LinearLayout>
- アプリを実行してテストします。
FlavorFragment
、PickupFragment
、SummaryFragment
のレイアウトにキャンセル ボタンが表示されるはずです。ただし、ボタンをタップしても、まだ何も起こりません。次のステップで、これらのボタンのクリック リスナーを設定します。
キャンセル ボタンのクリック リスナーを追加する
各フラグメント クラス(StartFragment
を除く)内に、キャンセル ボタンのクリックを処理するヘルパー メソッドを追加します。
- この
cancelOrder()
メソッドをFlavorFragment
に追加します。フレーバー オプションが表示されたとき、ユーザーが注文をキャンセルすることにした場合はsharedViewModel.resetOrder().
を呼び出してビューモデルを消去し、ID がR.id.action_flavorFragment_to_startFragment.
のナビゲーション アクションを使用してStartFragment
に戻ります。
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_flavorFragment_to_startFragment)
}
アクション リソース ID に関連するエラーが表示された場合は、nav_graph.xml
ファイルに戻って、ナビゲーション アクションの名前が同じ(action_flavorFragment_to_startFragment
)であるかどうかを確認します。
- リスナー バインディングを使用して、
fragment_flavor.xml
レイアウトのキャンセル ボタンにクリック リスナーを設定します。このボタンをクリックすると、先ほどFragmentFlavor
クラスに作成したcancelOrder()
メソッドが呼び出されます。
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
PickupFragment
について同じプロセスを繰り返します。cancelOrder()
メソッドをフラグメント クラスに追加します。このメソッドは、注文をリセットしてPickupFragment
からStartFragment
にナビゲートします。
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
fragment_pickup.xml
で、キャンセル ボタンのクリック リスナーを設定して、クリック時にcancelOrder()
メソッドを呼び出すようにします。
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
SummaryFragment
のキャンセル ボタンに同様のコードを追加し、ユーザーをStartFragment
に戻すようにします。androidx.navigation.fragment.findNavController
が自動的にインポートされない場合は、インポートする必要があります。.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
fragment_summary.xml
で、キャンセル ボタンがクリックされたときに、SummaryFragment
のcancelOrder()
メソッドを呼び出します。
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
- アプリを実行してテストし、各フラグメントに追加したロジックを検証します。カップケーキの注文の作成中に、
FlavorFragment
、PickupFragment
、SummaryFragment
でキャンセル ボタンをタップすると、StartFragment
に戻ります。新しい注文の作成を進めると、前回の注文の情報が消去されていることに気づくはずです。
これは正常な動作に見えますが、実際には、StartFragment
に戻ってから逆向きにナビゲートする際に、バグが発生します。バグを再現するため、次のステップを実施します。
- カップケーキの新しい注文を作成する注文フローを、概要画面が表示されるまで進めます。たとえば、12 個のカップケーキを注文し、チョコレート フレーバーを指定して、受け取り日を選択します。
- キャンセルをタップします。
StartFragment
に戻るはずです。 - 正常な動作に見えますが、システムの戻るボタンをタップすると、注文概要画面が 0 個のカップケーキ、フレーバーなしの状態で表示されます。これは誤りであり、ユーザーに表示するべきではありません。
ユーザーが望んだのは、注文フローを遡ることではないはずです。さらに、ビューモデルからの注文データがすべて消去されているので、表示される情報は役に立ちません。StartFragment
から戻るボタンがタップされたら、Cupcake アプリが終了することが求められます。
バックスタックが現在どのようになっているかを見て、バグを修正する方法を検討しましょう。注文概要画面で注文を作成すると、各デスティネーションがバックスタックにプッシュされます。
SummaryFragment
から注文をキャンセルしました。SummaryFragment
から StartFragment
へのアクションによってナビゲートすると、Android は新しいデスティネーションとして StartFragment
の別のインスタンスをバックスタックに追加しました。
そのため、StartFragment
から戻るボタンをタップすると、アプリは(空の注文情報で)SummaryFragment
を再度表示しました。
このナビゲーションのバグを修正するため、アクションによりナビゲートする際に Navigation コンポーネントを使用してバックスタックから追加のデスティネーションをポップする方法を学びます。
バックスタックから追加のデスティネーションをポップする
ナビゲーション アクション: popUpTo 属性
ナビゲーション グラフでナビゲーション アクションに app:popUpTo
属性を含めると、指定されたデスティネーションに到達するまで、複数のデスティネーションをバックスタックからポップできます。app:popUpTo="@id/startFragment"
を指定すると、StartFragment
に到達するまで、バックスタック内のデスティネーションがポップされ、StartFragment はスタックに残ります。
この変更をコードに追加してアプリを実行すると、注文をキャンセルしたときに StartFragment
に戻るようになります。しかし今度は、StartFragment
から戻るボタンをタップすると、(アプリが終了するのではなく)StartFragment
が再び表示されるようになります。これも望ましい動作ではありません。前述のように、StartFragment
にナビゲートしたので、Android は実際には新しいデスティネーションとして StartFragment
をバックスタックに追加します。そのため、バックスタックに StartFragment のインスタンスが 2 つ存在することになります。したがって、アプリを終了するには戻るボタンを 2 回タップする必要があります。
ナビゲーション アクション: popUpToInclusive 属性
この新たなバグを修正するには、StartFragment
までのすべてのデスティネーションをバックスタックからポップすることをリクエストします。これを行うには、app:popUpTo="@id/startFragment"
と
app:popUpToInclusive="true"
を適切なナビゲーション アクションに指定します。そうすると、バックスタック内に StartFragment
の新しいインスタンスが 1 つだけ作成されます。これにより、StartFragment
から戻るボタンを一度タップすると、アプリが終了します。実際に変更してみましょう。
ナビゲーション アクションを変更する
- res > navigation > nav_graph.xml ファイルを開いて、Navigation Editor に移動します。
summaryFragment
からstartFragment
にナビゲートするアクションを選択します(青色でハイライト表示されます)。- 右側の [Attributes] を展開します(まだ展開されていない場合)。変更できる属性のリストで、[Pop Behavior] を探します。
- プルダウン オプションで、[popUpTo] を
startFragment
に設定します。これは、バックスタック内のすべてのデスティネーションが(スタックの一番上から)startFragment
までポップされることを意味します。
- 次に、チェックマークと [true] のラベルが表示されるまで、[popUpToInclusive] のチェックボックスをクリックします。これは、バックスタックにすでに存在する
startFragment
のインスタンスを含めてデスティネーションをポップすることを示します。これにより、バックスタックにstartFragment
のインスタンスが 2 つ存在することがなくなります。
pickupFragment
をstartFragment
に接続するアクションについて、上記の変更を繰り返します。
flavorFragment
をstartFragment
に接続するアクションについて、上記の手順を繰り返します。- 完了したら、ナビゲーション グラフ ファイルのコードビューを表示して、アプリが正しく変更されていることを確認します。
<navigation
android:id="@+id/nav_graph" ...>
<fragment
android:id="@+id/startFragment" ...>
...
</fragment>
<fragment
android:id="@+id/flavorFragment" ...>
...
<action
android:id="@+id/action_flavorFragment_to_startFragment"
app:destination="@id/startFragment"
app:popUpTo="@id/startFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/pickupFragment" ...>
...
<action
android:id="@+id/action_pickupFragment_to_startFragment"
app:destination="@id/startFragment"
app:popUpTo="@id/startFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/summaryFragment" ...>
<action
android:id="@+id/action_summaryFragment_to_startFragment"
app:destination="@id/startFragment"
app:popUpTo="@id/startFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
3 つのアクション(action_flavorFragment_to_startFragment
、action_pickupFragment_to_startFragment
、action_summaryFragment_to_startFragment
)のそれぞれに、新たに追加した属性 app:popUpTo="@id/startFragment"
および app:popUpToInclusive="true"
があるはずです。
- アプリを実行します。注文フローを進めて、キャンセルをタップします。
StartFragment
に戻ったら、戻るボタンを一度だけタップして、アプリが終了することを確認します。
何が起こったかを要約します。注文をキャンセルしてアプリの最初の画面に戻ったとき、バックスタック内のすべてのフラグメント デスティネーション(StartFragment
の最初のインスタンスを含む)がスタックからポップされました。ナビゲーション アクションが完了すると、StartFragment
が新しいデスティネーションとしてバックスタックに追加されました。そこから戻るボタンをタップすると、StartFragment
がスタックからポップされ、バックスタックにフラグメントがなくなりました。そのため、Android はアクティビティを終了し、ユーザーの画面でアプリが閉じられました。
アプリは次のように表示されます。
5. 注文を送信する
以上で、アプリはかなり良くなりました。しかし、改良できる点がもうひとつあります。SummaryFragment
で注文送信ボタンをタップすると、依然として Toast
メッセージが表示されます。
アプリから注文を送信できれば、使い勝手がもっと良くなります。前の Codelab で学んだことを活用し、暗黙的インテントを使用してアプリと別のアプリの間で情報を共有しましょう。そうすれば、ユーザーはカップケーキの注文情報をデバイス上のメールアプリと共有して、ケーキ店に注文メールを送れるようになります。
この機能を実装するため、上のスクリーンショットでメールの件名と本文の構造を確認してください。
これらの文字列は strings.xml
ファイルにすでに含まれており、そこから使用できます。
<string name="new_cupcake_order">New Cupcake Order</string>
<string name="order_details">Quantity: %1$s cupcakes \n Flavor: %2$s \nPickup date: %3$s \n Total: %4$s \n\n Thank you!</string>
order_details
は、4 つの異なる書式引数を含む文字列リソースです。各引数は、カップケーキの実際の数量、希望するフレーバー、希望する受け取り日、合計金額のプレースホルダです。引数には 1 から 4 までの番号が付いており、構文は %1
~%4
です。引数の型も指定されています($s
は文字列が期待されることを意味します)。
Kotlin コードでは、R.string.order_details
の後に 4 つの引数を指定して、getString()
を呼び出すことができます(順序が重要です)。たとえば、getString(R.string.order_details, "12", "Chocolate", "Sat Dec 12", "$24.00")
を呼び出すと、次の文字列が作成されます。これは、まさに目的のメール本文です。
Quantity: 12 cupcakes Flavor: Chocolate Pickup date: Sat Dec 12 Total: $24.00 Thank you!
SummaryFragment.kt
で、sendOrder()
メソッドを変更します。既存のToast
メッセージを削除します。
fun sendOrder() {
}
sendOrder()
メソッド内で、注文概要テキストを作成します。共有ビューモデルから、注文数量、フレーバー、日付、金額を取得して、書式を持つorder_details
文字列を作成します。
val orderSummary = getString(
R.string.order_details,
sharedViewModel.quantity.value.toString(),
sharedViewModel.flavor.value.toString(),
sharedViewModel.date.value.toString(),
sharedViewModel.price.value.toString()
)
- 引き続き
sendOrder()
メソッド内で、他のアプリと注文を共有する暗黙的インテントを作成します。メール インテントを作成する方法については、ドキュメントをご覧ください。インテント アクションにIntent.ACTION_SEND
を指定し、型を"text/plain"
に設定して、インテント エクストラをメールの件名(Intent.EXTRA_SUBJECT
)とメールの本文(Intent.EXTRA_TEXT
)用に含めます。必要であればandroid.content.Intent
をインポートします。
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
.putExtra(Intent.EXTRA_TEXT, orderSummary)
ここで特別なヒントをお教えしましょう。このアプリを独自のユースケースに合わせて調整する場合、メールの受信者を事前入力してケーキ店のメールアドレスを指定できます。インテント内で、インテント エクストラ Intent.EXTRA_EMAIL
を使用してメール受信者を指定します。
- これは暗黙的インテントであるため、このインテントを処理する特定のコンポーネントまたはアプリを事前に認識する必要はありません。ユーザーは、インテントの実行に使用したいアプリを自分で決定できます。ただし、このインテントでアクティビティを起動する前に、それを処理できるアプリがあるかどうかをチェックしてください。このチェックにより、インテントを処理するアプリがない場合に Cupcake アプリがクラッシュすることが回避され、コードの安全性が向上します。
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
startActivity(intent)
}
このチェックを行うには、デバイスにインストールされているアプリ パッケージに関する情報が含まれている PackageManager
にアクセスします。activity
と packageManager
が null でなければ、フラグメントの activity
を介して PackageManager
にアクセスできます。作成したインテントで、PackageManager
の resolveActivity()
メソッドを呼び出します。結果が null でなければ、インテントを使用して安全に startActivity()
を呼び出すことができます。
- アプリを実行してコードをテストします。カップケーキの注文を作成し、[Send Order to Another App] をタップします。共有ダイアログが表示されたら、Gmail アプリを選択できます。または、必要に応じて別のアプリも選択できます。Gmail アプリを選択した場合、まだアカウントを設定していなければ、設定が必要になります(たとえばエミュレータを使用している場合)。メール本文に最新のカップケーキの注文が表示されない場合は、最初に現在のメールの下書きを破棄する必要があります。
さまざまなシナリオでテストすると、カップケーキを 1 個だけ注文する場合のバグに気づくはずです。注文概要に「1 cupcakes」と表示されますが、これは英語では文法的に誤りです。
正しくは「1 cupcake」(単数形)でなければなりません。数量の値に基づいて「cupcake」または「cupcakes」のいずれかを選択したい場合は、Android の数量文字列という機能を使用できます。plurals
リソースを宣言すると、数量の値に応じて、異なる文字列リソース(たとえば単数形と複数形)を指定できます。
strings.xml
ファイルに plurals リソースcupcakes
を追加します。
<plurals name="cupcakes">
<item quantity="one">%d cupcake</item>
<item quantity="other">%d cupcakes</item>
</plurals>
単数の場合(quantity="one"
)は、単数形の文字列が使用されます。それ以外の場合(quantity="other"
)は、複数形の文字列が使用されます。文字列の引数を期待する %s
と異なり、%d
は整数の引数を期待します。この引数は、文字列の書式設定の際に渡します。
Kotlin コードで、次の関数を呼び出します。
getQuantityString(R.plurals.cupcakes, 1, 1)
は文字列 1 cupcake
を返します。
getQuantityString(R.plurals.cupcakes, 6, 6)
は文字列 6 cupcakes
を返します。
getQuantityString(R.plurals.cupcakes, 0, 0)
は文字列 0 cupcakes
を返します。
- Kotlin コードに移る前に、
strings.xml
のorder_details
文字列リソースを更新して、ハードコードされた複数形の cupcakes を削除します。
<string name="order_details">Quantity: %1$s \n Flavor: %2$s \nPickup date: %3$s \n
Total: %4$s \n\n Thank you!</string>
SummaryFragment
クラスで、sendOrder()
メソッドを更新して新しい数量文字列を使用します。最初にビューモデルから数量を見つけて、それを変数に格納する方法が最も簡単です。ビューモデルのquantity
は型がLiveData<Int>
であるため、sharedViewModel.quantity.value
が null になる可能性があります。null の場合は、numberOfCupcakes
のデフォルト値として0
を使用します。
このロジックを sendOrder()
メソッドの最初の行のコードとして追加します。
val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
elvis 演算子(?:)は、左側の式が null でない場合はそれを使用することを意味します。左側の式が null の場合は、elvis 演算子の右側の式(この場合は 0
)が使用されます。
- 次に、先ほどと同様に
order_details
文字列を書式設定します。numberOfCupcakes
を数量引数として直接渡す代わりに、resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes)
を使用して、書式設定されたカップケーキ文字列を作成します。
完全な sendOrder()
メソッドは次のようになります。
fun sendOrder() {
val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
val orderSummary = getString(
R.string.order_details,
resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes),
sharedViewModel.flavor.value.toString(),
sharedViewModel.date.value.toString(),
sharedViewModel.price.value.toString()
)
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
.putExtra(Intent.EXTRA_TEXT, orderSummary)
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
startActivity(intent)
}
}
- コードを実行してテストします。メール本文の注文概要に、「1 cupcake」、「6 cupcakes」、または「12 cupcakes」と表示されることを確認します。
以上で、Cupcake アプリのすべての機能の実装が完了しました。おめでとうございます。これは挑戦しがいのあるアプリであり、あなたは Android 開発者になるための道のりにおいて飛躍的な進歩を遂げました。これまでに学んだすべてのコンセプトをうまく組み合わせることができただけでなく、その過程で問題解決の新しいヒントをいくつか得ることもできました。
最終ステップ
ここでは、コードのクリーンアップを行います。これは、前の Codelab で学んだように、コーディングの際の良い習慣です。
- インポートを最適化する
- ファイルを再フォーマットする
- 使用されていないコードまたはコメントアウトされたコードを削除する
- 必要に応じてコードにコメントを追加する
アプリのユーザー補助機能を強化するため、Talkback を有効にしてアプリをテストし、ユーザー エクスペリエンスがスムーズかどうかを確認します。音声フィードバックは、必要な場合に、画面上の各要素の目的をユーザーに伝達するのに役立ちます。また、スワイプ操作を使用してアプリのすべての要素をナビゲートできることを確認します。
実装したユースケースが最終版のアプリで想定どおりに動作することを再確認します。動作の例を次に示します。
- デバイスを回転させたときに(ビューモデルの機能により)データが保持される。
- 上へボタンまたは戻るボタンをタップしたとき、
FlavorFragment
とPickupFragment
で引き続き注文情報が正しく表示される。 - 別のアプリに注文を送信したとき、正しい注文詳細が共有される。
- 注文をキャンセルすると、その注文に含まれるすべての情報が消去される。
バグが見つかった場合は修正します。
おつかれさまです。以上で作業の再確認が問題なく完了しました。
6. 解答コード
この Codelab の解答コードは、下記のプロジェクトにあります。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開くまで待ちます。
- 実行ボタン をクリックし、アプリをビルドして実行します。正常にビルドされたことを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
7. 概要
- Android は、ユーザーがアクセスしたすべてのデスティネーションをバックスタックに保存し、それぞれの新しいデスティネーションをスタックにプッシュします。
- 上へボタンまたは戻るボタンをタップすると、バックスタックからデスティネーションをポップできます。
- Jetpack Navigation コンポーネントを使用すると、戻るボタンのデフォルトの動作を無償で利用して、フラグメント デスティネーションをバックスタックにプッシュし、バックスタックからポップできます。
- ナビゲーション グラフでアクションに
app:popUpTo
属性を指定すると、属性値で指定されたデスティネーションまでのデスティネーションをバックスタックからポップできます。 app:popUpTo
で指定したデスティネーションもバックスタックからポップする必要がある場合は、アクションにapp:popUpToInclusive="true"
を指定します。- 暗黙的インテントを作成してメールアプリとコンテンツを共有するには、
Intent.ACTION_SEND
を使用し、Intent.EXTRA_EMAIL
、Intent.EXTRA_SUBJECT
、Intent.EXTRA_TEXT
などのインテント エクストラを指定します。 - 数量に応じて異なる文字列リソース(単数形または複数形)を使用する場合は、
plurals
リソースを使用します。
8. 詳細
9. 自習用練習問題
Cupcake アプリを拡張して、カップケーキの注文フローに独自のバリエーションを加えてください。次に例を示します。
- 当日に受け取れないといった特別な条件がある特別なフレーバーを提供する。
- カップケーキを注文するユーザーの名前を尋ねる。
- 注文するカップケーキの数量が 1 個を超える場合、ユーザーがカップケーキのフレーバーを複数選択できるようにする。
こうした新しい機能に対応するために、アプリの更新が必要になる部分はどこでしょうか?
確認:
完成したアプリはエラーなしで動作する必要があります。
10. チャレンジ タスク
Cupcake アプリの作成を通して学んだことを活用して、独自のユースケース向けのアプリを作成してください。そのアプリでは、ピザやサンドイッチなど、いろいろなものを注文できます。実装を開始する前に、アプリのさまざまなデスティネーションの概要を決めておくことをおすすめします。
他のデザインのアイデアからインスピレーションを得るには、Shrine アプリが参考になります。これは、独自のブランドにマテリアルのテーマ設定とコンポーネントを導入する方法を示すマテリアルの練習用アプリです。Shrine アプリは、ここで作成した Cupcake アプリよりはるかに複雑です。したがって、最初から難しいアプリの構築を目指すのではなく、まずは小さな機能に取り組むことを検討してください。漸進的かつ段階的な成功を積み重ねて、徐々に自信を付けていきましょう。
独自のアプリが完成したら、作品をソーシャル メディアで共有しましょう。ハッシュタグ #LearningKotlin を付けて、検索できるようにしてください。