1. 始める前に
アクティビティ、フラグメント、インテント、データ バインディング、ナビゲーション コンポーネントの使用方法、アーキテクチャ コンポーネントの基本について学習しました。この Codelab では、すべてをまとめた高度なサンプルである、カップケーキの発注アプリに取り組んでいただきます。
共有 ViewModel
を使用して、同じアクティビティのフラグメント間や、LiveData
変換などの新しいコンセプト間でデータを共有する方法を学習します。
前提条件
- Android のレイアウトを XML で読んで理解することができる
- Jetpack Navigation コンポーネントの基本を理解している
- アプリ内でフラグメント デスティネーションを持つナビゲーション グラフを作成できる
- アクティビティ内でフラグメントを使用したことがある
ViewModel
を作成してアプリデータを保存できるLiveData
でデータ バインディングを使用して、ViewModel
にあるアプリデータを UI に反映して最新の状態に保つことができる
学習内容
- より高度なユースケースにおいて、推奨されるアプリ アーキテクチャのプラクティスを実装する方法
- アクティビティ内のフラグメント間で共有
ViewModel
を使用する方法 LiveData
変換の適用方法
作成するアプリの概要
- カップケーキの注文フローを表示する Cupcake アプリ。ユーザーはカップケーキのフレーバー、数量、受け取り日を選択できます。
必要なもの
- Android Studio がインストールされているパソコン
- Cupcake アプリのスターター コード。
2. スターター アプリの概要
Cupcake アプリの概要
カップケーキ アプリは、オンライン注文アプリを設計して実装する方法を示しています。このパスウェイが終了すると、以下の画面を持つ Cupcake アプリが完成します。ユーザーはカップケーキの注文時に数量、フレーバーなどのオプションを選択できます。
この Codelab のスターター コードをダウンロードする
この Codelab では、ここで学んだ機能を拡張するためのスターター コードを提供しています。スターター コードには、以前の Codelab でおなじみのコードが含まれています。
GitHub からスターター コードをダウンロードする場合、プロジェクトのフォルダ名が android-basics-kotlin-cupcake-app-starter
になっていることに注意してください。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] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
スターター コードのチュートリアル
- Android Studio でプロジェクトを開きます。プロジェクトのフォルダ名は
android-basics-kotlin-cupcake-app-starter
です。次にアプリを実行します。 - ファイルを参照してスターター コードを理解します。レイアウト ファイルの場合は、右上の [Split] オプションを使用して、レイアウトと XML のプレビューを同時に表示できます。
- アプリをコンパイルして実行すると、アプリが不完全であることがわかります。ボタンは(
Toast
メッセージの表示以外は)あまり機能せず、他のフラグメントに移動することもできません。
ここでは、プロジェクト内の重要なファイルについて説明します。
MainActivity:
MainActivity
には、デフォルトで生成されたコードと同様のコードがあり、アクティビティのコンテンツ ビューが activity_main.xml
に設定されています。このコードでは、パラメータ化されたコンストラクタ AppCompatActivity(@LayoutRes int contentLayoutId)
を使用します。このコンストラクタは super.onCreate(savedInstanceState)
の一部としてインフレートされるレイアウトを取り込みます。
MainActivity
クラスのコード
class MainActivity : AppCompatActivity(R.layout.activity_main)
これは、デフォルトの AppCompatActivity
コンストラクタを使用する次のコードと同じです。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
レイアウト(res/layout フォルダ):
layout
リソース フォルダには、アクティビティとフラグメントのレイアウト ファイルが含まれています。これらはシンプルなレイアウト ファイルであり、XML は前の Codelab でおなじみのものです。
fragment_start.xml
はアプリで最初に表示される画面です。カップケーキの画像と、注文するカップケーキの数を選択する 3 つのボタン(1 個、6 個、12 個)が表示されます。fragment_flavor.xml
では、カップケーキのフレーバーのリストがラジオボタン オプションとして表示され、[Next] ボタンが用意されています。fragment_pickup.xml
には、受け取り日を選択するオプションと、概要画面に移動するための [Next] ボタンが用意されています。fragment_summary.xml
には、数量やフレーバーなどの注文情報の概要と、注文を別のアプリに送信するためのボタンが表示されます。
Fragment クラス:
StartFragment.kt
はアプリで最初に表示される画面です。このクラスには、ビュー バインディング コードと 3 つのボタンのクリック ハンドラが含まれています。FlavorFragment.kt
、PickupFragment.kt
、SummaryFragment.kt
の各クラスは、ほとんどがボイラープレート コードと、[Next] ボタンまたは [Send Order to Another App] ボタンのクリック ハンドラで構成されます。
リソース(res フォルダ)
drawable
フォルダには、最初の画面の Cupcake アセットとランチャー アイコン ファイルが含まれています。navigation/nav_graph.xml
には、Action がない 4 つのフラグメント デスティネーション(startFragment
、flavorFragment
、pickupFragment
、summaryFragment
)が含まれています。Action は後で Codelab で定義します。values
フォルダには、アプリのテーマのカスタマイズに使用される色、ディメンション、文字列、スタイル、テーマが含まれます。こうしたリソースタイプについては、前の Codelab で理解しておく必要があります。
3. ナビゲーション グラフを完成させる
このタスクでは、Cupcake アプリの画面を結合して、アプリ内に適切なナビゲーションを実装します。
Navigation コンポーネントの使用に必要なものを覚えていますか?このガイドに従って、プロジェクトとアプリの設定方法を再確認しましょう。
- Jetpack Navigation ライブラリを追加する
- アクティビティに
NavHost
を追加する - ナビゲーション グラフを作成する
- ナビゲーション グラフにフラグメント デスティネーションを追加する
ナビゲーション グラフでデスティネーションを接続する
- Android Studio の [Project] ウィンドウで、res > navigation > nav_graph.xml ファイルを開きます。[Design] タブが選択されていない場合は、このタブに切り替えます。
- これで Navigation Editor が開き、アプリのナビゲーション グラフが可視化されます。アプリにすでに存在する 4 つのフラグメントが表示されます。
- ナビゲーション グラフでフラグメント デスティネーションを接続します。
startFragment
からflavorFragment
へのアクション、flavorFragment
からpickupFragment
への接続、pickupFragment
からsummaryFragment
への接続を作成します。詳しい手順が必要な場合は、次の手順をご覧ください。 - startFragment にカーソルを合わせると、フラグメントの周りにグレーの枠線が表示され、フラグメントの右端の中心に灰色の円が表示されます。円をクリックし、flavorFragment までドラッグしたらマウスを放します。
- 2 つのフラグメントの間にある矢印は、接続が成功したことを示します。つまり、startFragment から flavorFragment に移動できます。これは Navigation アクションと呼ばれるもので、前の Codelab で学習しました。
- 同様に、flavorFragment から pickupFragment へのナビゲーション アクションと、pickupFragment から summaryFragment へのナビゲーション アクションを追加します。ナビゲーション アクションの作成が完了すると、完成したナビゲーション グラフは次のようになります。
- 作成した新しい 3 つのアクションが [Component Tree] パネルにも反映されます。
- ナビゲーション グラフを定義するときは、開始デスティネーションも指定する必要があります。現在、startFragment の横に小さな家のアイコンがあることがわかります。
これは、startFragment が NavHost
に表示される最初のフラグメントであることを示します。これをアプリに必要な動作として残します。今後の参考として、フラグメントを右クリックしてメニュー オプションの [Set as Start Destination] を選択することで、いつでも開始デスティネーションを変更できます。
start フラグメントから flavor フラグメントに移動する
次に、Toast
メッセージを表示する代わりに、最初のフラグメントのボタンをタップすることで、startFragment から flavorFragment に移動するコードを追加します。以下は、start フラグメント レイアウトの参考例です。後のタスクで、カップケーキの数量を flavor フラグメントに渡します。
- [Project] ウィンドウで、app > java > com.example.cupcake > StartFragment の Kotlin ファイルを開きます。
onViewCreated()
メソッドでは、クリック リスナーが 3 つのボタンに設定されていることを確認します。各ボタンがタップされると、カップケーキの数量(1 個、2 個、または 6 個)をパラメータとして、orderCupcake()
メソッドが呼び出されます。
参照コード:
orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
orderCupcake()
メソッドで、トースト メッセージを表示するコードを、flavor フラグメントに移動するコードに変更します。findNavController()
メソッドを使用してNavController
を取得し、そこでnavigate()
を呼び出してアクション IDR.id.action_startFragment_to_flavorFragment
を渡します。このアクション ID がnav_graph.xml.
で宣言されたアクションと一致することを確認してください。
変更前のコード
fun orderCupcake(quantity: Int) {
Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}
変更後のコード
fun orderCupcake(quantity: Int) {
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
- インポート
import
androidx.navigation.fragment.findNavController
を追加するか、Android Studio に用意されているオプションから選択します。
Navigation を flavor と pickup のフラグメントに追加する
前のタスクと同様に、このタスクでは、別のフラグメント(flavor フラグメントと pickup フラグメント)にナビゲーションを追加します。
- app > java > com.example.cupcake > FlavorFragment.kt を開きます。[Next] ボタンのクリック リスナー内で呼び出されるメソッドが
goToNextScreen()
メソッドであることを確認してください。 FlavorFragment.kt
のgoToNextScreen()
メソッド内で、トーストを表示するコードを pickup フラグメントに移動するよう変更します。アクション ID としてR.id.action_flavorFragment_to_pickupFragment
を使用して、この ID がnav_graph.xml.
で宣言されたアクションと一致することを確認します。
fun goToNextScreen() {
findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}
import androidx.navigation.fragment.findNavController
を忘れないでください。
- 同様に、
PickupFragment.kt
のgoToNextScreen()
メソッド内で、既存のコードを summary フラグメントに移動するよう変更します。
fun goToNextScreen() {
findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}
androidx.navigation.fragment.findNavController
をインポートします。
- アプリを起動して、画面間を移動するためのボタンが機能することを確認してください。各フラグメントに表示される情報は不完全である可能性がありますが、以降のステップで正しいデータを入力します。
アプリバーのタイトルを更新する
アプリ内を移動する際に、アプリバーのタイトルに注目してください。常に Cupcake と表示されます。
現在のフラグメントの機能に基づいて、より関連性の高いタイトルを提供する方がユーザー エクスペリエンスが向上します。
NavController
を使用して各フラグメントのアプリバー(アクションバー)のタイトルを変更し、上へ(←)ボタンを表示します。
MainActivity.kt
で、onCreate()
メソッドをオーバーライドして、ナビゲーション コントローラを設定します。NavHostFragment
からNavController
のインスタンスを取得します。setupActionBarWithNavController(navController)
を呼び出して、NavController
のインスタンスを渡します。これにより、デスティネーションのラベルに基づいてアプリバーにタイトルが表示されます。最上位のデスティネーションにいない場合には上へボタンが表示されます。
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
}
- Android Studio のプロンプトが表示されたら、必要なインポートを追加します。
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
- 各フラグメントでアプリバーのタイトルを設定します。
navigation/nav_graph.xml
を開き、[Code] タブに切り替えます。 nav_graph.xml
で、各フラグメント デスティネーションのandroid:label
属性を変更します。スターター アプリですでに宣言されている、以下の文字列リソースを使用します。
start フラグメントの場合は、@string/app_name
と値 Cupcake
を使用します。
flavor フラグメントの場合は、@string/choose_flavor
と値 Choose Flavor
を使用します。
pickup フラグメントの場合は、@string/choose_pickup_date
と値 Choose Pickup Date
を使用します。
summary フラグメントの場合は、@string/order_summary
と値 Order Summary
を使用します。
<navigation ...>
<fragment
android:id="@+id/startFragment"
...
android:label="@string/app_name" ... >
<action ... />
</fragment>
<fragment
android:id="@+id/flavorFragment"
...
android:label="@string/choose_flavor" ... >
<action ... />
</fragment>
<fragment
android:id="@+id/pickupFragment"
...
android:label="@string/choose_pickup_date" ... >
<action ... />
</fragment>
<fragment
android:id="@+id/summaryFragment"
...
android:label="@string/order_summary" ... />
</navigation>
- アプリを実行します。各フラグメント デスティネーションに移動すると、アプリバーのタイトルが変化することがわかります。また、上へボタン(← 矢印)がアプリバーに表示されるようになりました。タップしても何も行われません。上へボタンの動作は次の Codelab で実装します。
4. 共有 ViewModel を作成する
それでは、それぞれのフラグメントに正しいデータを入力していきましょう。アプリのデータを単一の ViewModel
に保存するには、共有 ViewModel
を使用します。アプリ内の複数のフラグメントは、それぞれのアクティビティ スコープを使用して共有 ViewModel
にアクセスします。
ほとんどの製品版アプリでは、フラグメント間でデータを共有することが一般的なユースケースとなっています。たとえば、Cupcake アプリの最終バージョン(以下のスクリーンショットを参照)では、最初の画面でカップケーキの数量を選択して、2 番目の画面ではその数量に基づいて価格が計算されて表示されます。同様に、フレーバーや受け取り日といったその他のアプリデータも概要画面で使用されます。
アプリの機能から判断すると、この注文情報を単一の ViewModel
に保存し、このアクティビティのフラグメント間で共有できると便利であることがわかります。ViewModel
が Android アーキテクチャ コンポーネントの一部であることを再度確認します。ViewModel
内に保存されたアプリデータは、設定変更時にも保持されます。ViewModel
をアプリに追加するには、ViewModel
クラスから拡張する新しいクラスを作成します。
OrderViewModel を作成する
このタスクでは、OrderViewModel
という Cupcake アプリ用に共有 ViewModel
を作成します。また、ViewModel
内のプロパティとしてアプリデータを追加し、データを更新および変更するメソッドを追加します。クラスのプロパティは次のとおりです。
- 注文数(
Integer
) - カップケーキのフレーバー(
String
) - 受け取り日(
String
) - 料金(
Double
)
ViewModel
に関するおすすめの方法を実践する
ViewModel
では、ビューモデルのデータを public
変数として公開しないことをおすすめします。そうしないと、アプリデータが外部クラスで予期しない方法で変更され、アプリで想定していなかったエッジケースが発生する可能性があります。代わりに、変更可能なプロパティを private
とし、バッキング プロパティを実装して、必要に応じて public
な不変プロパティを各プロパティにおいて公開します。慣例として、変更可能な private
のプロパティ名の前にアンダースコア(_
)を付けます。
ユーザーの選択に応じて、上記のプロパティを更新するメソッドを以下に示します。
setQuantity(numberCupcakes: Int)
setFlavor(desiredFlavor: String)
setDate(pickupDate: String)
値段にはセッター メソッドは必要ありません。これは、他のプロパティを使用して OrderViewModel
内で計算するためです。共有 ViewModel
を実装する手順は次のとおりです。
プロジェクトに model
という新しいパッケージを作成し、OrderViewModel
クラスを追加します。これにより、ビューモデルのコードが残りの UI コード(フラグメントとアクティビティ)から分離されます。コーディング上では、機能に応じてコードをパッケージに分割することをおすすめします。
- Android Studio の [Project] ウィンドウで、[com.example.cupcake] > [New] > [Package] を右クリックします。
- [New Package] ダイアログが開いたら、パッケージ名を
com.example.cupcake.model
とします。
model
パッケージの下に Kotlin クラスのOrderViewModel
を作成します。[Project] ウィンドウでmodel
パッケージを右クリックし、[New] > [Kotlin File/Class] を選択します。新しいダイアログで、ファイル名をOrderViewModel
とします。
OrderViewModel.kt
で、クラスのシグネチャをViewModel
から拡張するように変更します。
import androidx.lifecycle.ViewModel
class OrderViewModel : ViewModel() {
}
OrderViewModel
クラス内に、上記のプロパティをprivate
val
として追加します。- プロパティ タイプを
LiveData
に変更し、そのプロパティにバッキング フィールドを追加することで、これらのプロパティが監視可能となり、ビューモデル内のソースデータが変更されたときに UI が更新されるようになります。
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity
private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor
private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date
private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price
下記のクラスをインポートする必要があります。
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
OrderViewModel
クラスに、上記のメソッドを追加します。メソッド内で、渡された引数を変更可能なプロパティに代入します。- これらのセッター メソッドはビューモデル外から呼び出す必要があるため、
public
メソッドのままにしてください(fun
キーワードの前にprivate
などの可視性修飾子は不要です)。Kotlin のデフォルトの可視性修飾子はpublic
です。
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
}
fun setFlavor(desiredFlavor: String) {
_flavor.value = desiredFlavor
}
fun setDate(pickupDate: String) {
_date.value = pickupDate
}
- アプリをビルドして実行し、コンパイル エラーがないことを確認します。まだ UI の表示に変化はありません。
お疲れさまでした。これで、ビューモデルのスタート地点に立ちました。構築するアプリの機能が増え、クラスに必要なプロパティやメソッドが増えてきたら、このクラスに段階的に追加していきます。
Android Studio でクラス名、プロパティ名、メソッド名がグレーのフォントで表示されていたら、それは想定内です。これは、そのクラス、プロパティ、メソッドが現在は使用されていませんが、これから使われるようになるということです。これについては、次回に説明します。
5. ViewModel を使用して UI を更新する
このタスクでは、作成した共有ビューモデルを使用して、アプリの UI を更新します。共有ビューモデルの実装で異なるのは、主に UI コントローラからのアクセス方法です。フラグメント インスタンスの代わりにアクティビティ インスタンスを使用します。その方法については、次のセクションで説明します。
つまり、ビューモデルはフラグメント間で共有できます。各フラグメントは、ビューモデルにアクセスして注文の詳細を確認したり、ビューモデルの一部のデータを更新したりできます。
ビューモデルを使用するように StartFragment を更新する
StartFragment
で共有ビューモデルを使用するには、viewModels()
デリゲート クラスではなく activityViewModels()
を使用して OrderViewModel
を初期化します。
viewModels()
は、現在のフラグメントを対象範囲とするViewModel
インスタンスを提供します。これはフラグメントごとに異なります。activityViewModels()
は、現在のアクティビティを対象範囲とするViewModel
インスタンスを提供します。したがってインスタンスは、同じアクティビティ内の複数のフラグメント間で同じになります。
Kotlin プロパティのデリゲートを使用する
Kotlin では、変更可能な各(var
)プロパティに、デフォルトのゲッター関数とセッター関数が自動的に生成されます。セッター関数とゲッター関数は、値を代入するか、プロパティの値を読み取るときに呼び出されます(読み取り専用プロパティ(val
)の場合、デフォルトではゲッター関数のみが生成されます。このゲッター関数は、読み取り専用プロパティの値を読み取るときに呼び出されます)。
Kotlin のプロパティ委任により、ゲッター / セッターの責任を別のクラスに引き継ぐことができます。
このクラス(委譲クラス)は、プロパティのゲッター関数とセッター関数を提供し、プロパティの変更を処理します。
デリゲート プロパティは、by
句とデリゲート クラス インスタンスを使用して定義されます。
// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
StartFragment
クラスでは、共有ビューモデルへの参照をクラス変数として取得します。fragment-ktx
ライブラリのby activityViewModels()
という Kotlin プロパティのデリゲートを使用します。
private val sharedViewModel: OrderViewModel by activityViewModels()
以下の新しいインポートが必要になる場合があります。
import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
FlavorFragment
クラス、PickupFragment
クラス、SummaryFragment
クラスに対して上記の手順を繰り返します。このsharedViewModel
インスタンスは、Codelab の後半セクションで使用します。- これで、
StartFragment
クラスに戻ると、ビューモデルを使用できます。orderCupcake()
メソッドの冒頭で、flavor フラグメントに移動する前に、共有ビューモデルのsetQuantity()
メソッドを呼び出して数量を更新します。
fun orderCupcake(quantity: Int) {
sharedViewModel.setQuantity(quantity)
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
OrderViewModel
クラス内に以下のメソッドを追加して、注文のフレーバーが設定されているかどうかを確認します。このメソッドは、後のステップのStartFragment
クラスで使用します。
fun hasNoFlavorSet(): Boolean {
return _flavor.value.isNullOrEmpty()
}
StartFragment
クラスのorderCupcake()
メソッド内で、数量を設定した後、フレーバーが設定されていない場合は、flavor フラグメントに移動する前に、デフォルトのフレーバーを Vanilla に設定します。完全なメソッドは次のようになります。
fun orderCupcake(quantity: Int) {
sharedViewModel.setQuantity(quantity)
if (sharedViewModel.hasNoFlavorSet()) {
sharedViewModel.setFlavor(getString(R.string.vanilla))
}
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
- アプリをビルドして、コンパイル エラーがないことを確認します。ただし、UI の表示に変化はありません。
6. データ バインディングで ViewModel を使用する
次に、データ バインディングを使用して、ビューモデルのデータを UI にバインドします。また、ユーザーが UI で選択した内容に基づいて共有ビューモデルを更新します。
データ バインディングを更新する
Data Binding ライブラリは Android Jetpack の一部です。データ バインディングでは、宣言的な形式を使用して、レイアウト内の UI コンポーネントをアプリのデータソースにバインドします。簡単に言うと、データ バインディングはビューに対する(コードの)データのバインディングと、ビュー バインディング(ビューをコードにバインドする)です。こうしたバインディングを設定し、更新を自動化しておくと、コードから手動で UI を更新するのを忘れた場合に、エラーになる可能性を減らすことができます。
ユーザーの選択でフレーバーを更新する
layout/fragment_flavor.xml
で、ルートの<layout>
タグ内に<data>
タグを追加します。com.example.cupcake.model.OrderViewModel
タイプのviewModel
という名前のレイアウト変数を追加します。type 属性のパッケージ名が、アプリの共有ビューモデル クラス(OrderViewModel
)のパッケージ名と一致していることを確認してください。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 同様に、
fragment_pickup.xml
、fragment_summary.xml
について上記の手順を繰り返し、viewModel
レイアウト変数を追加します。この変数は後のセクションで使用します。このレイアウトでは共有ビューモデルを使用しないため、fragment_start.xml
にこのコードを追加する必要はありません。 FlavorFragment
クラスのonViewCreated()
内で、ビューモデルのインスタンスをレイアウト内の共有ビューモデルのインスタンスとバインドします。binding?.
apply
ブロック内に次のコードを追加します。
binding?.apply {
viewModel = sharedViewModel
...
}
apply スコープ関数
Kotlin で apply
関数を見たのは初めてかもしれません。apply
は、Kotlin 標準ライブラリのスコープ関数です。オブジェクトのコンテキスト内でコードブロックを実行します。これは一時的なスコープを形成し、そのスコープの中で、名前のないオブジェクトにアクセスできます。apply
の一般的なユースケースは、オブジェクトの設定です。このような呼び出しは、「オブジェクトに次の割り当てを適用する」と解釈できます。
例:
clark.apply {
firstName = "Clark"
lastName = "James"
age = 18
}
// The equivalent code without apply scope function would look like the following.
clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
PickupFragment
クラスとSummaryFragment
クラス内のonViewCreated()
メソッドについても同じ手順を繰り返します。
binding?.apply {
viewModel = sharedViewModel
...
}
fragment_flavor.xml
では、新しいレイアウト変数viewModel
を使用して、ビューモデルのflavor
値に基づいてラジオボタンのchecked
属性を設定します。ラジオボタンで表されるフレーバーが、ビューモデルに保存されているフレーバーと同じ場合は、ラジオボタンが選択された状態(checked
= true)で表示されます。VanillaRadioButton
のオンにされた状態のバインディング式は以下のようになります。
@{viewModel.flavor.equals(@string/vanilla)}
基本的には、equals
関数を使用して viewModel.flavor
プロパティと対応する文字列リソースを比較し、オン / オフの状態が true か false かを判断します。
<RadioGroup
...>
<RadioButton
android:id="@+id/vanilla"
...
android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
.../>
<RadioButton
android:id="@+id/chocolate"
...
android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
.../>
<RadioButton
android:id="@+id/red_velvet"
...
android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
.../>
<RadioButton
android:id="@+id/salted_caramel"
...
android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
.../>
<RadioButton
android:id="@+id/coffee"
...
android:checked="@{viewModel.flavor.equals(@string/coffee)}"
.../>
</RadioGroup>
リスナー バインディング
リスナー バインディングは、onClick
イベントなどのイベント発生時に実行されるラムダ式です。textview.setOnClickListener(clickListener)
などのメソッド参照に似ていますが、リスナー バインディングでは任意のデータ バインディング式を実行できます。
fragment_flavor.xml
で、リスナー バインディングを使用してイベント リスナーをラジオボタンに追加します。パラメータを指定しないラムダ式を使用し、viewModel
.setFlavor()
メソッドを呼び出して、対応するフレーバーの文字列リソースを渡します。
<RadioGroup
...>
<RadioButton
android:id="@+id/vanilla"
...
android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
.../>
<RadioButton
android:id="@+id/chocolate"
...
android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
.../>
<RadioButton
android:id="@+id/red_velvet"
...
android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
.../>
<RadioButton
android:id="@+id/salted_caramel"
...
android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
.../>
<RadioButton
android:id="@+id/coffee"
...
android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
.../>
</RadioGroup>
- このアプリを実行して、flavor フラグメントで [Vanilla] オプションがデフォルトで選択されていることを確認します。
よくできました。これで、次のフラグメントに移ることができます。
7. ビューモデルを使用するように pickup と summary の各フラグメントを更新する
アプリ内を移動すると、pickup フラグメントのラジオボタン オプションのラベルが空白になることがわかります。このタスクでは、得られる 4 つの受け取り日を計算し、pickup フラグメントに表示します。書式付きの日付を表示するにはさまざまな方法がありますが、ここでは Android が提供する便利なユーティリティをいくつか紹介します。
受け取りオプション リストを作成する
日付フォーマッタ
Android フレームワークでは SimpleDateFormat
という名前のクラスが用意されています。これは、ロケールに依存した方法で日付の書式設定や解析を行うためのクラスです。日付の書式設定(日付 → テキスト)や解析(テキスト → 日付)が可能です。
SimpleDateFormat
のインスタンスを作成するには、パターン文字列とロケールを渡します。
SimpleDateFormat("E MMM d", Locale.getDefault())
"E MMM d"
のようなパターン文字列は、日付と時刻の形式を表します。'A'
から 'Z'
までの文字と 'a'
から 'z'
までの文字は、日付や時刻の文字列の構成要素を表すパターン文字として解釈されます。たとえば、d
は日、y
は年、M
は月を表します。日付が 2018 年 1 月 4 日の場合、パターン文字列 "EEE, MMM d"
は "Wed, Jul 4"
に解析されます。パターンの一覧については、こちらのドキュメントをご覧ください。
Locale
オブジェクトは、特定の地理的、政治的、文化的な地域を表します。これは、言語、国、バリアントの組み合わせを表します。ロケールは、数字や日付などの情報の表示を、その地域の慣習に合わせて変更するために使用します。日付と時刻は、世界各地で表記が異なるため、ロケールに依存しています。メソッド Locale.getDefault()
を使用して、ユーザーのデバイスに設定されているロケール情報を取得し、SimpleDateFormat
コンストラクタに渡します。
Android のロケールは、言語と国コードの組み合わせです。言語コードは 2 つの小文字で構成される ISO 言語コード(英語は「en」など)です。国コードは 2 つの大文字で構成される ISO 国コード(米国は「US」など)です。
ここで、SimpleDateFormat
と Locale
を使用して、Cupcake アプリの利用可能な受け取り日を判断します。
OrderViewModel
クラスに、getPickupOptions()
という名前の以下の関数を追加して、受け取り日のリストを作成して返します。メソッド内で、options
というval
変数を作成し、mutableListOf
<String>()
に初期化します。
private fun getPickupOptions(): List<String> {
val options = mutableListOf<String>()
}
SimpleDateFormat
にパターン文字列"E MMM d"
とロケールを渡して、フォーマッタ文字列を作成します。パターン文字列では、E
は曜日名を表し、「Tue Dec 10」に解析されます。
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
Android Studio のプロンプトが表示されたら、java.text.SimpleDateFormat
と java.util.Locale
をインポートします。
Calendar
インスタンスを取得して、新しい変数に代入します。変数はval
にします。この変数には現在の日付と時刻が含まれます。java.util.Calendar
もインポートします。
val calendar = Calendar.getInstance()
- 現在の日付から 3 日間の日付のリストを作成します。4 つの日付オプションが必要なため、このコードブロックを 4 回繰り返してください。この
repeat
ブロックは、日付を書式設定して日付オプションのリストに追加してから、カレンダーの日を 1 つ加算します。
repeat(4) {
options.add(formatter.format(calendar.time))
calendar.add(Calendar.DATE, 1)
}
- メソッドの最後に、更新された
options
を返します。完成したメソッドは以下のとおりです。
private fun getPickupOptions(): List<String> {
val options = mutableListOf<String>()
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
val calendar = Calendar.getInstance()
// Create a list of dates starting with the current date and the following 3 dates
repeat(4) {
options.add(formatter.format(calendar.time))
calendar.add(Calendar.DATE, 1)
}
return options
}
OrderViewModel
クラスに、val
であるdateOptions
という名前のクラス プロパティを追加します。作成したばかりのgetPickupOptions()
メソッドを使用してこれを初期化します。
val dateOptions = getPickupOptions()
レイアウトを更新して受け取りオプションを表示する
ビューモデルに 4 つの受け取り日を用意できたので、fragment_pickup.xml
レイアウトを更新してこれらの日付を表示します。また、データ バインディングを使用して、各ラジオボタンのチェック状態を表示したり、別のラジオボタンが選択されたときにビューモデルの日付を更新したりします。この実装は、flavor フラグメントのデータ バインディングと同様です。
fragment_pickup.xml
内:
ラジオボタン option0
は viewModel
の dateOptions[0]
(今日)を表します。
ラジオボタン option1
は viewModel
の dateOptions[1]
(明日)を表します。
ラジオボタン option2
は viewModel
の dateOptions[2]
(明後日)を表します。
ラジオボタン option3
は viewModel
の dateOptions[3]
(明明後日)を表します。
fragment_pickup.xml
のoption0
ラジオボタンで、新しいレイアウト変数viewModel
を使用して、ビューモデルのdate
値に基づいてchecked
属性を設定します。viewModel.date
プロパティとdateOptions
リストの最初の文字列(現在の日付)を比較します。equals
関数を使用して比較すると、最終的なバインディング式は以下のようになります。
@{viewModel.date.equals(viewModel.dateOptions[0])}
- 同じラジオボタンの場合、
onClick
属性へのリスナー バインディングを使用してイベント リスナーを追加します。このラジオボタンのオプションがクリックされたら、viewModel
でsetDate()
を呼び出して、dateOptions[0]
を渡します。 - 同じラジオボタンの場合、
text
属性の値をdateOptions
リストの最初の文字列に設定します。
<RadioButton
android:id="@+id/option0"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
android:text="@{viewModel.dateOptions[0]}"
...
/>
- 他のラジオボタンについても上記の手順を繰り返し、それに応じて
dateOptions
のインデックスを変更します。
<RadioButton
android:id="@+id/option1"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
android:text="@{viewModel.dateOptions[1]}"
... />
<RadioButton
android:id="@+id/option2"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
android:text="@{viewModel.dateOptions[2]}"
... />
<RadioButton
android:id="@+id/option3"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
android:text="@{viewModel.dateOptions[3]}"
... />
- このアプリを実行すると、利用可能な受け取りオプションとしてこれから数日が表示されます。スクリーンショットは、現在の日付に応じて異なります。デフォルトで選択されているオプションはありません。これは次のステップで実装します。
OrderViewModel
クラス内にresetOrder()
という名前の関数を作成し、ビューモデルのMutableLiveData
プロパティをリセットします。dateOptions
リストから現在の日付の値を_date.
value.
に代入します。
fun resetOrder() {
_quantity.value = 0
_flavor.value = ""
_date.value = dateOptions[0]
_price.value = 0.0
}
init
ブロックをクラスに追加し、そこから新しいメソッドresetOrder()
を呼び出します。
init {
resetOrder()
}
- クラス内のプロパティの宣言から初期値を削除します。これで、
OrderViewModel
のインスタンスが作成されたときに、init
ブロックを使用してプロパティを初期化できるようになりました。
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity
private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor
private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date
private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
- アプリを再度実行すると、デフォルトで今日の日付が選択されていることがわかると思います。
ビューモデルを使用するように Summary フラグメントを更新する
それでは、最後のフラグメントに進みましょう。Order Summary フラグメントは、注文情報の概要を表示するためのものです。このタスクでは、共有ビューモデルのすべての注文情報を活用し、データ バインディングを使用して画面上の注文の詳細を更新します。
fragment_summary.xml
で、ビューモデルのデータ変数viewModel
が宣言されていることを確認します。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
SummaryFragment
のonViewCreated()
で、binding.viewModel
が初期化されていることを確認します。fragment_summary.xml
のビューモデルから読み込んで、画面を注文の概要情報で更新します。以下のテキスト属性を追加して、数量、フレーバー、日付のTextViews
を更新します。数量はInt
型なので、文字列に変換する必要があります。
<TextView
android:id="@+id/quantity"
...
android:text="@{viewModel.quantity.toString()}"
... />
<TextView
android:id="@+id/flavor"
...
android:text="@{viewModel.flavor}"
... />
<TextView
android:id="@+id/date"
...
android:text="@{viewModel.date}"
... />
- アプリを実行してテストし、選択した注文オプションが注文の概要画面に表示されていることを確認します。
8. 注文の詳細から価格を計算する
この Codelab の最終的なアプリのスクリーンショットを見ると、価格が各フラグメント(StartFragment
を除く)に実際に表示されるため、ユーザーは注文を行う際に価格を知ることができます。
価格の計算方法に関するカップケーキ ショップのルールは以下のとおりです。
- 各カップケーキはそれぞれ 2.00 ドル
- 同日受け取りの場合は、注文に 3.00 ドルの追加料金
そのため、カップケーキを 6 個注文する場合、価格は 6 個 × 2 ドル = 12 ドルとなります。同日に受け取る場合は、追加料金 3 ドルが加算され、注文総額は 15 ドルになります。
ビューモデルの価格を更新する
この機能のサポートをアプリに追加するには、まずカップケーキの 1 個あたりの価格だけを考えて、当日受け取り費用は無視します。
OrderViewModel.kt
を開き、カップケーキ 1 個あたりの価格を変数に格納します。ファイルの先頭において、クラス定義の外側(ただし import ステートメントの後)でトップレベルのプライベート定数として宣言します。const
修飾子を使用し、読み取り専用にするためにval
にします。
package ...
import ...
private const val PRICE_PER_CUPCAKE = 2.00
class OrderViewModel : ViewModel() {
...
再確認ですが、定数値(Kotlin で const
キーワードとしてマーク)は変更されず、値はコンパイル時に認識されます。定数の詳細については、こちらのドキュメントをご覧ください。
- カップケーキ 1 個あたりの価格を定義したら、価格を計算するヘルパー メソッドを作成します。このメソッドはこのクラス内でのみ使用されるため、
private
にできます。次のタスクで、当日受け取り料金を含む価格ロジックに変更します。
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}
このコード行は、カップケーキ 1 個あたりの価格に、注文したカップケーキの数量を掛けます。括弧内のコードでは、quantity.value
の値は null の可能性があるため、elvis 演算子(?:
)を使用します。elvis 演算子(?:
)の使い方は、左側の式が null でない場合はそれを使用し、左側の式が null の場合は、elvis 演算子の右側の式(この場合は 0
)を使用します。
- 同じ
OrderViewModel
クラスで、数量の設定時に価格変数を更新します。setQuantity()
関数の中で新しい関数を呼び出します。
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
updatePrice()
}
price プロパティを UI にバインドする
fragment_flavor.xml
、fragment_pickup.xml
、fragment_summary.xml
のレイアウトで、com.example.cupcake.model.OrderViewModel
タイプのデータ変数viewModel
が定義されていることを確認します。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 各フラグメント クラスの
onViewCreated()
メソッドで、フラグメント内のビューモデル オブジェクトのインスタンスをレイアウト内のビューモデル データ変数にバインドすることを確認します。
binding?.apply {
viewModel = sharedViewModel
...
}
- 各フラグメントのレイアウト内で、
viewModel
変数を使用して、レイアウトに表示されている場合は価格を設定します。まず、fragment_flavor.xml
ファイルを変更します。subtotal
テキストビューでは、android:text
属性の値を"@{@string/subtotal_price(viewModel.price)}".
に設定します。このデータ バインディングのレイアウト式で、文字列リソース@string/subtotal_price
を使用し、パラメータ(ビューモデルからの価格)を渡すため、「Subtotal 12.0」のように出力表示されます。
...
<TextView
android:id="@+id/subtotal"
android:text="@{@string/subtotal_price(viewModel.price)}"
... />
...
使用するこの文字列リソースは、strings.xml
ファイルですでに宣言されています。
<string name="subtotal_price">Subtotal %s</string>
- アプリを実行し、start フラグメントで [One cupcake] を選択した場合、flavor フラグメントに「Subtotal 2.0」と表示されます。[Six cupcakes] を選択した場合は、flavor フラグメントに「Subtotal 12.0」などと表示されます。価格を適切な通貨形式に設定するのは後で行うため、現時点でこの動作は想定どおりです。
- 次に、pickup フラグメントと summary フラグメントにも同様の変更を加えます。
fragment_pickup.xml
レイアウトとfragment_summary.xml
レイアウトで、viewModel
price
プロパティも使用するようにテキストビューを変更します。
fragment_pickup.xml
...
<TextView
android:id="@+id/subtotal"
...
android:text="@{@string/subtotal_price(viewModel.price)}"
... />
...
fragment_summary.xml
…
<TextView
android:id="@+id/total"
...
android:text="@{@string/total_price(viewModel.price)}"
... />
…
- アプリを実行して、注文の概要に表示される価格が、1 個、6 個、12 個のカップケーキ注文数量に対して、正しく計算されていることを確認します。前述のとおり、現時点では価格の表示形式は正しくない結果となります(2 ドルの場合は 2.0、12 ドルの場合は 12.0 と表示されます)。
同日受け取りの場合の追加料金
このタスクでは、2 つ目のルールを実装します。これは、注文と同じ日に受け取る場合、注文に対して 3.00 ドルの追加料金が発生するというものです。
OrderViewModel
クラスでは、同日受け取り料金を表す新しいトップレベルのプライベート定数を定義します。
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
updatePrice()
で、ユーザーが同日受け取りを選択したかどうかを確認します。ビューモデルの日付(_date.
value
)が、常に現在の日付であるdateOptions
リストの最初のアイテムと同じかどうかを確認します。
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
if (dateOptions[0] == _date.value) {
}
}
- こうした計算を簡単にするために、一時的な変数
calculatedPrice
を導入します。更新された価格を計算し、これを再び_price.
value
に代入します。
private fun updatePrice() {
var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
// If the user selected the first option (today) for pickup, add the surcharge
if (dateOptions[0] == _date.value) {
calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
}
_price.value = calculatedPrice
}
setDate()
メソッドからupdatePrice()
ヘルパー メソッドを呼び出して、同日受け取り料金を追加します。
fun setDate(pickupDate: String) {
_date.value = pickupDate
updatePrice()
}
- アプリを実行し、アプリ内を移動すると、受け取り日を変更しても、当日受け取り料金が合計金額から削除されないことがわかります。ビューモデルで価格は変更されたものの、バインディング レイアウトには通知されないためです。
LiveData を監視するようにライフサイクル オーナーを設定する
LifecycleOwner
は、Android のライフサイクル(アクティビティやフラグメントなど)を持つクラスです。LiveData
オブザーバーは、ライフサイクル所有者が有効な状態(STARTED
または RESUMED
)の場合に限り、アプリのデータに対する変更を監視します。
アプリでは、LiveData
オブジェクトまたは監視可能なデータは、ビューモデルの price
プロパティです。ライフサイクルのオーナーは、flavor、pickup、summary の各フラグメントです。LiveData
オブザーバーは、価格などの監視可能なデータを含むレイアウト ファイル内のバインディング式です。データ バインディングを使用すると、監視可能な値が変更されたときに、バインドされている UI 要素が自動的に更新されます。
バインディング式の例: android:text="@{@string/subtotal_price(viewModel.price)}"
UI 要素を自動的に更新するには、binding.
lifecycleOwner
を
アプリのライフサイクル所有者に関連付ける必要があります。次はこれを実装します。
FlavorFragment
クラス、PickupFragment
クラス、SummaryFragment
クラスのonViewCreated()
メソッド内で、binding?.apply
ブロックに以下を追加します。これにより、バインディング オブジェクトにライフサイクル所有者が設定されます。ライフサイクル オーナーを設定すると、アプリでLiveData
オブジェクトを監視できるようになります。
binding?.apply {
lifecycleOwner = viewLifecycleOwner
...
}
- アプリを再度実行します。受け取り画面で、受け取り日を変更すると、以前とは異なって価格が自動的に変更されるようになります。また、受け取り料金が概要画面に正しく反映されます。
- 受け取りに本日の日付を選択すると、注文価格が 3 ドル増えることがわかります。本日以降の日付を選択した場合、価格はカップケーキの数量 x 2.00 ドルのままになります。
- カップケーキの数量、フレーバー、受け取り日を変えて、さまざまなケースをテストします。これで、各フラグメントのビューモデルから価格が更新されていることがわかります。最大の利点は、毎回 UI を最新の価格で更新するために追加の Kotlin コードを記述する必要がないということです。
価格機能の実装を完了するには、価格を現地通貨に合わせる必要があります。
LiveData 変換で価格の表示形式を設定する
LiveData
変換メソッドにより、ソース LiveData
に対するデータ操作を行い、結果の LiveData
オブジェクトを返すことができます。簡単に言うと、LiveData
の値を別の値に変換します。こうした変換は、オブザーバーが LiveData
オブジェクトを監視していない限り、計算されません。
Transformations.map()
は変換関数の 1 つであり、このメソッドはソース LiveData
と関数をパラメータとして受け取ります。この関数はソースの LiveData
を操作し、やはり監視可能である更新値を返します。
LiveData 変換を使用できるリアルタイムの例を以下に示します。
- 表示する日付、時刻の文字列の形式を設定する
- アイテムのリストを並べ替える
- アイテムをフィルタまたはグループ化する
- すべてのアイテムの合計、アイテム数など、リストから結果を計算し、最後のアイテムを返す
このタスクでは、Transformations.map()
メソッドを使用して、現地通貨を使用するように価格の形式を設定します。元の価格は 10 進値(LiveData<Double>
)から文字列値(LiveData<String>
)に変換します。
OrderViewModel
クラスで、バッキング プロパティ タイプをLiveData<Double>.
ではなくLiveData<String>
に変更します。形式設定された価格は「$」などの通貨記号が付いた文字列になります。次のステップでは、初期化エラーを修正します。
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
Transformations.map()
を使用して新しい変数を初期化し、_price
とラムダ関数を渡します。NumberFormat
クラスのgetCurrencyInstance()
メソッドを使用して、価格を現地通貨形式に変換します。変換コードは以下のようになります。
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
NumberFormat.getCurrencyInstance().format(it)
}
androidx.lifecycle.Transformations
と java.text.NumberFormat
をインポートする必要があります。
- アプリを実行すると、小計と合計金額に表示形式が設定された価格文字列が表示されます。これで、以前よりもはるかにユーザー フレンドリーな UI になりました。
- 期待どおりに動作するかどうかをテストします。テストケースは次のようになります(例: カップケーキを 1 個注文、6 個注文、12 個注文)。各画面で価格が正しく更新されていることを確認します。Flavor フラグメントと Pickup フラグメントには「Subtotal $2.00」、Order Summary には「Total $2.00」のように表示されます。また、注文の概要画面に正しい注文内容が表示されているか確認します。
9. リスナー バインディングを使用してクリック リスナーを設定する
このタスクでは、リスナー バインディングを使用して、フラグメント クラス内のボタンクリック リスナーをレイアウトにバインドします。
- レイアウト ファイル
fragment_start.xml
で、com.example.cupcake.StartFragment
型のstartFragment
というデータ変数を追加します。フラグメントのパッケージ名とアプリのパッケージ名が一致することを確認します。
<layout ...>
<data>
<variable
name="startFragment"
type="com.example.cupcake.StartFragment" />
</data>
...
<ScrollView ...>
StartFragment.kt
のonViewCreated()
メソッドで、新しいデータ変数をフラグメント インスタンスにバインドします。this
キーワードを使用して、フラグメント内のフラグメント インスタンスにアクセスできます。binding?.
apply
ブロックと、その中のコードを削除します。完成したメソッドは次のようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.startFragment = this
}
fragment_start.xml
で、ボタンのonClick
属性へのリスナー バインディングを使用してイベント リスナーを追加し、startFragment
でorderCupcake()
を呼び出してカップケーキの数量を渡します。
<Button
android:id="@+id/order_one_cupcake"
android:onClick="@{() -> startFragment.orderCupcake(1)}"
... />
<Button
android:id="@+id/order_six_cupcakes"
android:onClick="@{() -> startFragment.orderCupcake(6)}"
... />
<Button
android:id="@+id/order_twelve_cupcakes"
android:onClick="@{() -> startFragment.orderCupcake(12)}"
... />
- アプリを実行し、start フラグメントのボタンクリック ハンドラが期待どおりに動作していることを確認します。
- 同様に、上記のデータ変数を他のレイアウトにも追加して、フラグメント インスタンス、
fragment_flavor.xml
、fragment_pickup.xml
、fragment_summary.xml
をバインドします。
fragment_flavor.xml
内
<layout ...>
<data>
<variable
... />
<variable
name="flavorFragment"
type="com.example.cupcake.FlavorFragment" />
</data>
<ScrollView ...>
fragment_pickup.xml
内:
<layout ...>
<data>
<variable
... />
<variable
name="pickupFragment"
type="com.example.cupcake.PickupFragment" />
</data>
<ScrollView ...>
fragment_summary.xml
内:
<layout ...>
<data>
<variable
... />
<variable
name="summaryFragment"
type="com.example.cupcake.SummaryFragment" />
</data>
<ScrollView ...>
- 残りのフラグメント クラスの
onViewCreated()
メソッドで、ボタンのクリック リスナーを手動で設定するコードを削除します。 onViewCreated()
メソッドで、フラグメントのデータ変数をフラグメント インスタンスにバインドします。ここではthis
キーワードの使用方法が異なります。これは、binding?.apply
ブロック内でキーワードthis
がフラグメント インスタンスではなくバインディング インスタンスを参照しているためです。@
を使用して、フラグメント クラス名を明示的に指定します(例:this@FlavorFragment
)。完成したonViewCreated()
メソッドは以下のようになります。
FlavorFragment
クラスの onViewCreated()
メソッドは以下のようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
flavorFragment = this@FlavorFragment
}
}
PickupFragment
クラスの onViewCreated()
メソッドは以下のようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
pickupFragment = this@PickupFragment
}
}
結果として得られる SummaryFragment
クラスメソッドの onViewCreated()
メソッドは以下のようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
summaryFragment = this@SummaryFragment
}
}
- 他のレイアウト ファイルと同様に、リスナー バインディング式をボタンの
onClick
属性に追加します。
fragment_flavor.xml
内:
<Button
android:id="@+id/next_button"
android:onClick="@{() -> flavorFragment.goToNextScreen()}"
... />
fragment_pickup.xml
内:
<Button
android:id="@+id/next_button"
android:onClick="@{() -> pickupFragment.goToNextScreen()}"
... />
fragment_summary.xml
内:
<Button
android:id="@+id/send_button"
android:onClick="@{() -> summaryFragment.sendOrder()}"
...>
- アプリを実行して、ボタンが想定どおりに動作することを確認します。目に見える動作の変化はありませんが、これでリスナー バインディングを使用してクリック リスナーを設定したことになります。
お疲れさまでした。Codelab を完了し、Cupcake アプリを作成しました。しかし、アプリはまだ完成していません。次の Codelab で [Cancel] ボタンを追加し、バックスタックを変更します。また、バックスタックとは何か、その他の新しいトピックについても学習します。ご参加をお待ちしております。
10. 解答コード
この Codelab の解答コードは、下記のプロジェクトにあります。viewmodel ブランチを使用して、コードを取得またはダウンロードします。
この 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] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
11. まとめ
ViewModel
は Android アーキテクチャ コンポーネントの一部であり、ViewModel
内に保存されているデータは設定変更時にも保持されます。ViewModel
をアプリに追加するには、新しいクラスを作成して、ViewModel
クラスから拡張します。- 共有
ViewModel
は、複数のフラグメントのアプリデータを 1 つのViewModel
に保存するために使用されます。アプリ内の複数のフラグメントは、それぞれのアクティビティ スコープを使用して共有ViewModel
にアクセスします。 LifecycleOwner
は、Android のライフサイクル(アクティビティやフラグメントなど)を持つクラスです。LiveData
オブザーバーは、ライフサイクル所有者が有効な状態(STARTED
またはRESUMED
)の場合に限り、アプリのデータに対する変更を監視します。- リスナー バインディングは、
onClick
イベントなどのイベント発生時に実行されるラムダ式です。textview.setOnClickListener(clickListener)
などのメソッド参照に似ていますが、リスナー バインディングでは任意のデータ バインディング式を実行できます。 LiveData
変換メソッドにより、ソースLiveData
に対するデータ操作を行い、結果のLiveData
オブジェクトを返すことができます。- Android フレームワークでは
SimpleDateFormat
という名前のクラスが用意されています。これは、ロケールに依存した方法で日付の書式設定や解析を行うためのクラスです。日付の書式設定(日付 → テキスト)や解析(テキスト → 日付)が可能です。