フラグメント間の共有 ViewModel

1. 始める前に

アクティビティ、フラグメント、インテント、データ バインディング、ナビゲーション コンポーネントの使用方法、アーキテクチャ コンポーネントの基本について学習しました。この Codelab では、すべてをまとめた高度なサンプルである、カップケーキの発注アプリに取り組んでいただきます。

共有 ViewModel を使用して、同じアクティビティのフラグメント間や、LiveData 変換などの新しいコンセプト間でデータを共有する方法を学習します。

前提条件

  • Android のレイアウトを XML で読んで理解することができる
  • Jetpack Navigation コンポーネントの基本を理解している
  • アプリ内でフラグメント デスティネーションを持つナビゲーション グラフを作成できる
  • アクティビティ内でフラグメントを使用したことがある
  • ViewModel を作成してアプリデータを保存できる
  • LiveData でデータ バインディングを使用して、ViewModel にあるアプリデータを UI に反映して最新の状態に保つことができる

学習内容

  • より高度なユースケースにおいて、推奨されるアプリ アーキテクチャのプラクティスを実装する方法
  • アクティビティ内のフラグメント間で共有 ViewModel を使用する方法
  • LiveData 変換の適用方法

作成するアプリの概要

  • カップケーキの注文フローを表示する Cupcake アプリ。ユーザーはカップケーキのフレーバー、数量、受け取り日を選択できます。

必要なもの

2. スターター アプリの概要

Cupcake アプリの概要

カップケーキ アプリは、オンライン注文アプリを設計して実装する方法を示しています。このパスウェイが終了すると、以下の画面を持つ Cupcake アプリが完成します。ユーザーはカップケーキの注文時に数量、フレーバーなどのオプションを選択できます。

732881cfc463695d.png

この Codelab のスターター コードをダウンロードする

この Codelab では、ここで学んだ機能を拡張するためのスターター コードを提供しています。スターター コードには、以前の Codelab でおなじみのコードが含まれています。

GitHub からスターター コードをダウンロードする場合、プロジェクトのフォルダ名が android-basics-kotlin-cupcake-app-starter になっていることに注意してください。Android Studio でプロジェクトを開いたときに、このフォルダを選択します。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

5b0a76c50478a73f.png

  1. ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

36cc44fcf0f89a1d.png

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

21f3eec988dcfbe9.png

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開くまで待ちます。
  4. 実行ボタン 11c34fc5e516fb1c.png をクリックし、アプリをビルドして実行します。正常にビルドされたことを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。

スターター コードのチュートリアル

  1. Android Studio でプロジェクトを開きます。プロジェクトのフォルダ名は android-basics-kotlin-cupcake-app-starter です。次にアプリを実行します。
  2. ファイルを参照してスターター コードを理解します。レイアウト ファイルの場合は、右上の [Split] オプションを使用して、レイアウトと XML のプレビューを同時に表示できます。
  3. アプリをコンパイルして実行すると、アプリが不完全であることがわかります。ボタンは(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.ktPickupFragment.ktSummaryFragment.kt の各クラスは、ほとんどがボイラープレート コードと、[Next] ボタンまたは [Send Order to Another App] ボタンのクリック ハンドラで構成されます。

リソース(res フォルダ)

  • drawable フォルダには、最初の画面の Cupcake アセットとランチャー アイコン ファイルが含まれています。
  • navigation/nav_graph.xml には、Action がない 4 つのフラグメント デスティネーション(startFragmentflavorFragmentpickupFragmentsummaryFragment)が含まれています。Action は後で Codelab で定義します。
  • values フォルダには、アプリのテーマのカスタマイズに使用される色、ディメンション、文字列、スタイル、テーマが含まれます。こうしたリソースタイプについては、前の Codelab で理解しておく必要があります。

3. ナビゲーション グラフを完成させる

このタスクでは、Cupcake アプリの画面を結合して、アプリ内に適切なナビゲーションを実装します。

Navigation コンポーネントの使用に必要なものを覚えていますか?このガイドに従って、プロジェクトとアプリの設定方法を再確認しましょう。

  • Jetpack Navigation ライブラリを追加する
  • アクティビティに NavHost を追加する
  • ナビゲーション グラフを作成する
  • ナビゲーション グラフにフラグメント デスティネーションを追加する

ナビゲーション グラフでデスティネーションを接続する

  1. Android Studio の [Project] ウィンドウで、res > navigation > nav_graph.xml ファイルを開きます。[Design] タブが選択されていない場合は、このタブに切り替えます。

28c2c94eb97e2f0.png

  1. これで Navigation Editor が開き、アプリのナビゲーション グラフが可視化されます。アプリにすでに存在する 4 つのフラグメントが表示されます。

fdce89b318218ea6.png

  1. ナビゲーション グラフでフラグメント デスティネーションを接続します。startFragment から flavorFragment へのアクション、flavorFragment から pickupFragment への接続、pickupFragment から summaryFragment への接続を作成します。詳しい手順が必要な場合は、次の手順をご覧ください。
  2. startFragment にカーソルを合わせると、フラグメントの周りにグレーの枠線が表示され、フラグメントの右端の中心に灰色の円が表示されます。円をクリックし、flavorFragment までドラッグしたらマウスを放します。

d014c1b710c1088d.png

  1. 2 つのフラグメントの間にある矢印は、接続が成功したことを示します。つまり、startFragment から flavorFragment に移動できます。これは Navigation アクションと呼ばれるもので、前の Codelab で学習しました。

65c7d993b98c9dea.png

  1. 同様に、flavorFragment から pickupFragment へのナビゲーション アクションと、pickupFragment から summaryFragment へのナビゲーション アクションを追加します。ナビゲーション アクションの作成が完了すると、完成したナビゲーション グラフは次のようになります。

724eb8992a1a9381.png

  1. 作成した新しい 3 つのアクションが [Component Tree] パネルにも反映されます。

e4ee54469f5ff1a4.png

  1. ナビゲーション グラフを定義するときは、開始デスティネーションも指定する必要があります。現在、startFragment の横に小さな家のアイコンがあることがわかります。

739d4ddac561c478.png

これは、startFragmentNavHost に表示される最初のフラグメントであることを示します。これをアプリに必要な動作として残します。今後の参考として、フラグメントを右クリックしてメニュー オプションの [Set as Start Destination] を選択することで、いつでも開始デスティネーションを変更できます。

bf3cfa7841476892.png

次に、Toast メッセージを表示する代わりに、最初のフラグメントのボタンをタップすることで、startFragment から flavorFragment に移動するコードを追加します。以下は、start フラグメント レイアウトの参考例です。後のタスクで、カップケーキの数量を flavor フラグメントに渡します。

867d8e4c72078f76.png

  1. [Project] ウィンドウで、app > java > com.example.cupcake > StartFragment の Kotlin ファイルを開きます。
  2. onViewCreated() メソッドでは、クリック リスナーが 3 つのボタンに設定されていることを確認します。各ボタンがタップされると、カップケーキの数量(1 個、2 個、または 6 個)をパラメータとして、orderCupcake() メソッドが呼び出されます。

参照コード:

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. orderCupcake() メソッドで、トースト メッセージを表示するコードを、flavor フラグメントに移動するコードに変更します。findNavController() メソッドを使用して NavController を取得し、そこで navigate() を呼び出してアクション ID R.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)
}
  1. インポート import androidx.navigation.fragment.findNavController を追加するか、Android Studio に用意されているオプションから選択します。

2a087f53a77765a6.png

Navigation を flavor と pickup のフラグメントに追加する

前のタスクと同様に、このタスクでは、別のフラグメント(flavor フラグメントと pickup フラグメント)にナビゲーションを追加します。

3b351067bf4926b7.png

  1. app > java > com.example.cupcake > FlavorFragment.kt を開きます。[Next] ボタンのクリック リスナー内で呼び出されるメソッドが goToNextScreen() メソッドであることを確認してください。
  2. FlavorFragment.ktgoToNextScreen() メソッド内で、トーストを表示するコードを 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 を忘れないでください。

  1. 同様に、PickupFragment.ktgoToNextScreen() メソッド内で、既存のコードを summary フラグメントに移動するよう変更します。
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

androidx.navigation.fragment.findNavController をインポートします。

  1. アプリを起動して、画面間を移動するためのボタンが機能することを確認してください。各フラグメントに表示される情報は不完全である可能性がありますが、以降のステップで正しいデータを入力します。

96b33bf7a5bd8050.png

アプリバーのタイトルを更新する

アプリ内を移動する際に、アプリバーのタイトルに注目してください。常に Cupcake と表示されます。

現在のフラグメントの機能に基づいて、より関連性の高いタイトルを提供する方がユーザー エクスペリエンスが向上します。

NavController を使用して各フラグメントのアプリバー(アクションバー)のタイトルを変更し、上へ(←)ボタンを表示します。

b7657cdc50cfeab0.png

  1. MainActivity.kt で、onCreate() メソッドをオーバーライドして、ナビゲーション コントローラを設定します。NavHostFragment から NavController のインスタンスを取得します。
  2. 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)
    }
}
  1. Android Studio のプロンプトが表示されたら、必要なインポートを追加します。
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. 各フラグメントでアプリバーのタイトルを設定します。navigation/nav_graph.xml を開き、[Code] タブに切り替えます。
  2. 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>
  1. アプリを実行します。各フラグメント デスティネーションに移動すると、アプリバーのタイトルが変化することがわかります。また、上へボタン(← 矢印)がアプリバーに表示されるようになりました。タップしても何も行われません。上へボタンの動作は次の Codelab で実装します。

89e0ea37d4146271.png

4. 共有 ViewModel を作成する

それでは、それぞれのフラグメントに正しいデータを入力していきましょう。アプリのデータを単一の ViewModel に保存するには、共有 ViewModel を使用します。アプリ内の複数のフラグメントは、それぞれのアクティビティ スコープを使用して共有 ViewModel にアクセスします。

ほとんどの製品版アプリでは、フラグメント間でデータを共有することが一般的なユースケースとなっています。たとえば、Cupcake アプリの最終バージョン(以下のスクリーンショットを参照)では、最初の画面でカップケーキの数量を選択して、2 番目の画面ではその数量に基づいて価格が計算されて表示されます。同様に、フレーバーや受け取り日といったその他のアプリデータも概要画面で使用されます。

3b6a68cab0b9ee2.png

アプリの機能から判断すると、この注文情報を単一の ViewModel に保存し、このアクティビティのフラグメント間で共有できると便利であることがわかります。ViewModelAndroid アーキテクチャ コンポーネントの一部であることを再度確認します。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 コード(フラグメントとアクティビティ)から分離されます。コーディング上では、機能に応じてコードをパッケージに分割することをおすすめします。

  1. Android Studio の [Project] ウィンドウで、[com.example.cupcake] > [New] > [Package] を右クリックします。
  2. [New Package] ダイアログが開いたら、パッケージ名を com.example.cupcake.model とします。

d958ee5f3d2aef5a.png

  1. model パッケージの下に Kotlin クラスの OrderViewModel を作成します。[Project] ウィンドウで model パッケージを右クリックし、[New] > [Kotlin File/Class] を選択します。新しいダイアログで、ファイル名を OrderViewModel とします。

fc68c1d3861f1cca.png

  1. OrderViewModel.kt で、クラスのシグネチャを ViewModel から拡張するように変更します。
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. OrderViewModel クラス内に、上記のプロパティを private val として追加します。
  2. プロパティ タイプを 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
  1. OrderViewModel クラスに、上記のメソッドを追加します。メソッド内で、渡された引数を変更可能なプロパティに代入します。
  2. これらのセッター メソッドはビューモデル外から呼び出す必要があるため、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
}
  1. アプリをビルドして実行し、コンパイル エラーがないことを確認します。まだ 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>()
  1. StartFragment クラスでは、共有ビューモデルへの参照をクラス変数として取得します。fragment-ktx ライブラリの by activityViewModels() という Kotlin プロパティのデリゲートを使用します。
private val sharedViewModel: OrderViewModel by activityViewModels()

以下の新しいインポートが必要になる場合があります。

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. FlavorFragment クラス、PickupFragment クラス、SummaryFragment クラスに対して上記の手順を繰り返します。この sharedViewModel インスタンスは、Codelab の後半セクションで使用します。
  2. これで、StartFragment クラスに戻ると、ビューモデルを使用できます。orderCupcake() メソッドの冒頭で、flavor フラグメントに移動する前に、共有ビューモデルの setQuantity() メソッドを呼び出して数量を更新します。
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. OrderViewModel クラス内に以下のメソッドを追加して、注文のフレーバーが設定されているかどうかを確認します。このメソッドは、後のステップの StartFragment クラスで使用します。
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. 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)
}
  1. アプリをビルドして、コンパイル エラーがないことを確認します。ただし、UI の表示に変化はありません。

6. データ バインディングで ViewModel を使用する

次に、データ バインディングを使用して、ビューモデルのデータを UI にバインドします。また、ユーザーが UI で選択した内容に基づいて共有ビューモデルを更新します。

データ バインディングを更新する

Data Binding ライブラリAndroid Jetpack の一部です。データ バインディングでは、宣言的な形式を使用して、レイアウト内の UI コンポーネントをアプリのデータソースにバインドします。簡単に言うと、データ バインディングはビューに対する(コードの)データのバインディングと、ビュー バインディング(ビューをコードにバインドする)です。こうしたバインディングを設定し、更新を自動化しておくと、コードから手動で UI を更新するのを忘れた場合に、エラーになる可能性を減らすことができます。

ユーザーの選択でフレーバーを更新する

  1. 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 ...>

    ...
  1. 同様に、fragment_pickup.xmlfragment_summary.xml について上記の手順を繰り返し、viewModel レイアウト変数を追加します。この変数は後のセクションで使用します。このレイアウトでは共有ビューモデルを使用しないため、fragment_start.xml にこのコードを追加する必要はありません。
  2. 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
  1. PickupFragment クラスと SummaryFragment クラス内の onViewCreated() メソッドについても同じ手順を繰り返します。
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. fragment_flavor.xml では、新しいレイアウト変数 viewModel を使用して、ビューモデルの flavor 値に基づいてラジオボタンの checked 属性を設定します。ラジオボタンで表されるフレーバーが、ビューモデルに保存されているフレーバーと同じ場合は、ラジオボタンが選択された状態(checked = true)で表示されます。Vanilla RadioButton のオンにされた状態のバインディング式は以下のようになります。

@{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) などのメソッド参照に似ていますが、リスナー バインディングでは任意のデータ バインディング式を実行できます。

  1. 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>
  1. このアプリを実行して、flavor フラグメントで [Vanilla] オプションがデフォルトで選択されていることを確認します。

3095e824b4817b98.png

よくできました。これで、次のフラグメントに移ることができます。

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」など)です。

ここで、SimpleDateFormatLocale を使用して、Cupcake アプリの利用可能な受け取り日を判断します。

  1. OrderViewModel クラスに、getPickupOptions() という名前の以下の関数を追加して、受け取り日のリストを作成して返します。メソッド内で、options という val 変数を作成し、mutableListOf<String>() に初期化します。
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. SimpleDateFormat にパターン文字列 "E MMM d" とロケールを渡して、フォーマッタ文字列を作成します。パターン文字列では、E は曜日名を表し、「Tue Dec 10」に解析されます。
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

Android Studio のプロンプトが表示されたら、java.text.SimpleDateFormatjava.util.Locale をインポートします。

  1. Calendar インスタンスを取得して、新しい変数に代入します。変数は val にします。この変数には現在の日付と時刻が含まれます。java.util.Calendar もインポートします。
val calendar = Calendar.getInstance()
  1. 現在の日付から 3 日間の日付のリストを作成します。4 つの日付オプションが必要なため、このコードブロックを 4 回繰り返してください。この repeat ブロックは、日付を書式設定して日付オプションのリストに追加してから、カレンダーの日を 1 つ加算します。
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  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
}
  1. OrderViewModel クラスに、val である dateOptions という名前のクラス プロパティを追加します。作成したばかりの getPickupOptions() メソッドを使用してこれを初期化します。
val dateOptions = getPickupOptions()

レイアウトを更新して受け取りオプションを表示する

ビューモデルに 4 つの受け取り日を用意できたので、fragment_pickup.xml レイアウトを更新してこれらの日付を表示します。また、データ バインディングを使用して、各ラジオボタンのチェック状態を表示したり、別のラジオボタンが選択されたときにビューモデルの日付を更新したりします。この実装は、flavor フラグメントのデータ バインディングと同様です。

fragment_pickup.xml 内:

ラジオボタン option0viewModeldateOptions[0](今日)を表します。

ラジオボタン option1viewModeldateOptions[1](明日)を表します。

ラジオボタン option2viewModeldateOptions[2](明後日)を表します。

ラジオボタン option3viewModeldateOptions[3](明明後日)を表します。

  1. fragment_pickup.xmloption0 ラジオボタンで、新しいレイアウト変数 viewModel を使用して、ビューモデルの date 値に基づいて checked 属性を設定します。viewModel.date プロパティと dateOptions リストの最初の文字列(現在の日付)を比較します。equals 関数を使用して比較すると、最終的なバインディング式は以下のようになります。

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. 同じラジオボタンの場合、onClick 属性へのリスナー バインディングを使用してイベント リスナーを追加します。このラジオボタンのオプションがクリックされたら、viewModelsetDate() を呼び出して、dateOptions[0] を渡します。
  2. 同じラジオボタンの場合、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]}"
   ...
   />
  1. 他のラジオボタンについても上記の手順を繰り返し、それに応じて 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]}"
   ... />
  1. このアプリを実行すると、利用可能な受け取りオプションとしてこれから数日が表示されます。スクリーンショットは、現在の日付に応じて異なります。デフォルトで選択されているオプションはありません。これは次のステップで実装します。

b55b3a36e2aa7be6.png

  1. OrderViewModel クラス内に resetOrder() という名前の関数を作成し、ビューモデルの MutableLiveData プロパティをリセットします。dateOptions リストから現在の日付の値を _date.value. に代入します。
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. init ブロックをクラスに追加し、そこから新しいメソッド resetOrder() を呼び出します。
init {
   resetOrder()
}
  1. クラス内のプロパティの宣言から初期値を削除します。これで、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
  1. アプリを再度実行すると、デフォルトで今日の日付が選択されていることがわかると思います。

bfe4f1b82977b4bc.png

ビューモデルを使用するように Summary フラグメントを更新する

それでは、最後のフラグメントに進みましょう。Order Summary フラグメントは、注文情報の概要を表示するためのものです。このタスクでは、共有ビューモデルのすべての注文情報を活用し、データ バインディングを使用して画面上の注文の詳細を更新します。

78f510e10d848dd2.png

  1. fragment_summary.xml で、ビューモデルのデータ変数 viewModel が宣言されていることを確認します。
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. SummaryFragmentonViewCreated() で、binding.viewModel が初期化されていることを確認します。
  2. 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}"
   ... />
  1. アプリを実行してテストし、選択した注文オプションが注文の概要画面に表示されていることを確認します。

7091453fa817b55.png

8. 注文の詳細から価格を計算する

この Codelab の最終的なアプリのスクリーンショットを見ると、価格が各フラグメント(StartFragment を除く)に実際に表示されるため、ユーザーは注文を行う際に価格を知ることができます。

3b6a68cab0b9ee2.png

価格の計算方法に関するカップケーキ ショップのルールは以下のとおりです。

  • 各カップケーキはそれぞれ 2.00 ドル
  • 同日受け取りの場合は、注文に 3.00 ドルの追加料金

そのため、カップケーキを 6 個注文する場合、価格は 6 個 × 2 ドル = 12 ドルとなります。同日に受け取る場合は、追加料金 3 ドルが加算され、注文総額は 15 ドルになります。

ビューモデルの価格を更新する

この機能のサポートをアプリに追加するには、まずカップケーキの 1 個あたりの価格だけを考えて、当日受け取り費用は無視します。

  1. OrderViewModel.kt を開き、カップケーキ 1 個あたりの価格を変数に格納します。ファイルの先頭において、クラス定義の外側(ただし import ステートメントの後)でトップレベルのプライベート定数として宣言します。const 修飾子を使用し、読み取り専用にするために val にします。
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

再確認ですが、定数値(Kotlin で const キーワードとしてマーク)は変更されず、値はコンパイル時に認識されます。定数の詳細については、こちらのドキュメントをご覧ください。

  1. カップケーキ 1 個あたりの価格を定義したら、価格を計算するヘルパー メソッドを作成します。このメソッドはこのクラス内でのみ使用されるため、private にできます。次のタスクで、当日受け取り料金を含む価格ロジックに変更します。
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

このコード行は、カップケーキ 1 個あたりの価格に、注文したカップケーキの数量を掛けます。括弧内のコードでは、quantity.value の値は null の可能性があるため、elvis 演算子(?:)を使用します。elvis 演算子(?:)の使い方は、左側の式が null でない場合はそれを使用し、左側の式が null の場合は、elvis 演算子の右側の式(この場合は 0)を使用します。

  1. 同じ OrderViewModel クラスで、数量の設定時に価格変数を更新します。setQuantity() 関数の中で新しい関数を呼び出します。
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

price プロパティを UI にバインドする

  1. fragment_flavor.xmlfragment_pickup.xmlfragment_summary.xml のレイアウトで、com.example.cupcake.model.OrderViewModel タイプのデータ変数 viewModel が定義されていることを確認します。
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. 各フラグメント クラスの onViewCreated() メソッドで、フラグメント内のビューモデル オブジェクトのインスタンスをレイアウト内のビューモデル データ変数にバインドすることを確認します。
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. 各フラグメントのレイアウト内で、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>
  1. アプリを実行し、start フラグメントで [One cupcake] を選択した場合、flavor フラグメントに「Subtotal 2.0」と表示されます。[Six cupcakes] を選択した場合は、flavor フラグメントに「Subtotal 12.0」などと表示されます。価格を適切な通貨形式に設定するのは後で行うため、現時点でこの動作は想定どおりです。

  1. 次に、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. アプリを実行して、注文の概要に表示される価格が、1 個、6 個、12 個のカップケーキ注文数量に対して、正しく計算されていることを確認します。前述のとおり、現時点では価格の表示形式は正しくない結果となります(2 ドルの場合は 2.0、12 ドルの場合は 12.0 と表示されます)。

同日受け取りの場合の追加料金

このタスクでは、2 つ目のルールを実装します。これは、注文と同じ日に受け取る場合、注文に対して 3.00 ドルの追加料金が発生するというものです。

  1. OrderViewModel クラスでは、同日受け取り料金を表す新しいトップレベルのプライベート定数を定義します。
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. updatePrice() で、ユーザーが同日受け取りを選択したかどうかを確認します。ビューモデルの日付(_date.value)が、常に現在の日付である dateOptions リストの最初のアイテムと同じかどうかを確認します。
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. こうした計算を簡単にするために、一時的な変数 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
}
  1. setDate() メソッドから updatePrice() ヘルパー メソッドを呼び出して、同日受け取り料金を追加します。
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. アプリを実行し、アプリ内を移動すると、受け取り日を変更しても、当日受け取り料金が合計金額から削除されないことがわかります。ビューモデルで価格は変更されたものの、バインディング レイアウトには通知されないためです。

2ea8e000fb4e6ec8.png

LiveData を監視するようにライフサイクル オーナーを設定する

LifecycleOwner は、Android のライフサイクル(アクティビティやフラグメントなど)を持つクラスです。LiveData オブザーバーは、ライフサイクル所有者が有効な状態(STARTED または RESUMED)の場合に限り、アプリのデータに対する変更を監視します。

アプリでは、LiveData オブジェクトまたは監視可能なデータは、ビューモデルの price プロパティです。ライフサイクルのオーナーは、flavor、pickup、summary の各フラグメントです。LiveData オブザーバーは、価格などの監視可能なデータを含むレイアウト ファイル内のバインディング式です。データ バインディングを使用すると、監視可能な値が変更されたときに、バインドされている UI 要素が自動的に更新されます。

バインディング式の例: android:text="@{@string/subtotal_price(viewModel.price)}"

UI 要素を自動的に更新するには、binding.lifecycleOwner

アプリのライフサイクル所有者に関連付ける必要があります。次はこれを実装します。

  1. FlavorFragment クラス、PickupFragment クラス、SummaryFragment クラスの onViewCreated() メソッド内で、binding?.apply ブロックに以下を追加します。これにより、バインディング オブジェクトにライフサイクル所有者が設定されます。ライフサイクル オーナーを設定すると、アプリで LiveData オブジェクトを監視できるようになります。
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. アプリを再度実行します。受け取り画面で、受け取り日を変更すると、以前とは異なって価格が自動的に変更されるようになります。また、受け取り料金が概要画面に正しく反映されます。
  2. 受け取りに本日の日付を選択すると、注文価格が 3 ドル増えることがわかります。本日以降の日付を選択した場合、価格はカップケーキの数量 x 2.00 ドルのままになります。

  1. カップケーキの数量、フレーバー、受け取り日を変えて、さまざまなケースをテストします。これで、各フラグメントのビューモデルから価格が更新されていることがわかります。最大の利点は、毎回 UI を最新の価格で更新するために追加の Kotlin コードを記述する必要がないということです。

f4c0a3c5ea916d03.png

価格機能の実装を完了するには、価格を現地通貨に合わせる必要があります。

LiveData 変換で価格の表示形式を設定する

LiveData 変換メソッドにより、ソース LiveData に対するデータ操作を行い、結果の LiveData オブジェクトを返すことができます。簡単に言うと、LiveData の値を別の値に変換します。こうした変換は、オブザーバーが LiveData オブジェクトを監視していない限り、計算されません。

Transformations.map() は変換関数の 1 つであり、このメソッドはソース LiveData と関数をパラメータとして受け取ります。この関数はソースの LiveData を操作し、やはり監視可能である更新値を返します。

LiveData 変換を使用できるリアルタイムの例を以下に示します。

  • 表示する日付、時刻の文字列の形式を設定する
  • アイテムのリストを並べ替える
  • アイテムをフィルタまたはグループ化する
  • すべてのアイテムの合計、アイテム数など、リストから結果を計算し、最後のアイテムを返す

このタスクでは、Transformations.map() メソッドを使用して、現地通貨を使用するように価格の形式を設定します。元の価格は 10 進値(LiveData<Double>)から文字列値(LiveData<String>)に変換します。

  1. OrderViewModel クラスで、バッキング プロパティ タイプを LiveData<Double>. ではなく LiveData<String> に変更します。形式設定された価格は「$」などの通貨記号が付いた文字列になります。次のステップでは、初期化エラーを修正します。
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. Transformations.map() を使用して新しい変数を初期化し、_price とラムダ関数を渡します。NumberFormat クラスの getCurrencyInstance() メソッドを使用して、価格を現地通貨形式に変換します。変換コードは以下のようになります。
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

androidx.lifecycle.Transformationsjava.text.NumberFormat をインポートする必要があります。

  1. アプリを実行すると、小計と合計金額に表示形式が設定された価格文字列が表示されます。これで、以前よりもはるかにユーザー フレンドリーな UI になりました。

1853bd13a07f1bc7.png

  1. 期待どおりに動作するかどうかをテストします。テストケースは次のようになります(例: カップケーキを 1 個注文、6 個注文、12 個注文)。各画面で価格が正しく更新されていることを確認します。Flavor フラグメントと Pickup フラグメントには「Subtotal $2.00」、Order Summary には「Total $2.00」のように表示されます。また、注文の概要画面に正しい注文内容が表示されているか確認します。

9. リスナー バインディングを使用してクリック リスナーを設定する

このタスクでは、リスナー バインディングを使用して、フラグメント クラス内のボタンクリック リスナーをレイアウトにバインドします。

  1. レイアウト ファイル fragment_start.xml で、com.example.cupcake.StartFragment 型の startFragment というデータ変数を追加します。フラグメントのパッケージ名とアプリのパッケージ名が一致することを確認します。
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. StartFragment.ktonViewCreated() メソッドで、新しいデータ変数をフラグメント インスタンスにバインドします。this キーワードを使用して、フラグメント内のフラグメント インスタンスにアクセスできます。binding?.apply ブロックと、その中のコードを削除します。完成したメソッドは次のようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. fragment_start.xml で、ボタンの onClick 属性へのリスナー バインディングを使用してイベント リスナーを追加し、startFragmentorderCupcake() を呼び出してカップケーキの数量を渡します。
<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)}"
    ... />
  1. アプリを実行し、start フラグメントのボタンクリック ハンドラが期待どおりに動作していることを確認します。
  2. 同様に、上記のデータ変数を他のレイアウトにも追加して、フラグメント インスタンス、fragment_flavor.xmlfragment_pickup.xmlfragment_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 ...>
  1. 残りのフラグメント クラスの onViewCreated() メソッドで、ボタンのクリック リスナーを手動で設定するコードを削除します。
  2. 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
   }
}
  1. 他のレイアウト ファイルと同様に、リスナー バインディング式をボタンの 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()}"
    ...>
  1. アプリを実行して、ボタンが想定どおりに動作することを確認します。目に見える動作の変化はありませんが、これでリスナー バインディングを使用してクリック リスナーを設定したことになります。

お疲れさまでした。Codelab を完了し、Cupcake アプリを作成しました。しかし、アプリはまだ完成していません。次の Codelab で [Cancel] ボタンを追加し、バックスタックを変更します。また、バックスタックとは何か、その他の新しいトピックについても学習します。ご参加をお待ちしております。

10. 解答コード

この Codelab の解答コードは、下記のプロジェクトにあります。viewmodel ブランチを使用して、コードを取得またはダウンロードします。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

5b0a76c50478a73f.png

  1. ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

36cc44fcf0f89a1d.png

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

21f3eec988dcfbe9.png

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開くまで待ちます。
  4. 実行ボタン 11c34fc5e516fb1c.png をクリックし、アプリをビルドして実行します。正常にビルドされたことを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。

11. まとめ

  • ViewModelAndroid アーキテクチャ コンポーネントの一部であり、ViewModel 内に保存されているデータは設定変更時にも保持されます。ViewModel をアプリに追加するには、新しいクラスを作成して、ViewModel クラスから拡張します。
  • 共有 ViewModel は、複数のフラグメントのアプリデータを 1 つの ViewModel に保存するために使用されます。アプリ内の複数のフラグメントは、それぞれのアクティビティ スコープを使用して共有 ViewModel にアクセスします。
  • LifecycleOwner は、Android のライフサイクル(アクティビティやフラグメントなど)を持つクラスです。
  • LiveData オブザーバーは、ライフサイクル所有者が有効な状態(STARTED または RESUMED)の場合に限り、アプリのデータに対する変更を監視します。
  • リスナー バインディングは、onClick イベントなどのイベント発生時に実行されるラムダ式です。textview.setOnClickListener(clickListener) などのメソッド参照に似ていますが、リスナー バインディングでは任意のデータ バインディング式を実行できます。
  • LiveData 変換メソッドにより、ソース LiveData に対するデータ操作を行い、結果の LiveData オブジェクトを返すことができます。
  • Android フレームワークでは SimpleDateFormat という名前のクラスが用意されています。これは、ロケールに依存した方法で日付の書式設定や解析を行うためのクラスです。日付の書式設定(日付 → テキスト)や解析(テキスト → 日付)が可能です。

12. 関連リンク