共有ビューモデル

  • 投稿者:
  • 投稿カテゴリー:その他

共有ビューモデル

基本的にはフラグメント個別のビューモデルと共有ビューモデルの内容は同じ

複数のフラグメント間でデータやメソッドを共有する。
基本的にはフラグメント個別のビューモデルと共有ビューモデルの内容は同じ
model等の名前のパッケージを作成しViewModel型のクラスファイルを作成し、画面間で共有する変数をクラス変数として作成
共有する変数は可変の「」で始まる変数と読取専用の「」なしのものを用意。データの変更を監視するにはライブデータの変数とする。

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import java.text.NumberFormat
import java.util.*
import java.text.SimpleDateFormat

private const val PRICE_PER_CUPCAKE = 200.00
private const val PRICE_FOR_SAME_DAY_PICKUP = 300.00

// model等のパッケージを作成しViewModel型のクラスファイルを作成
class OrderViewModel : ViewModel(){

    // 画面間で共有する変数をクラス変数として作成
    // データの変更を検知するためLiveData型とする
    // 共有する変数は可変の「_」で始まる変数と読取専用の「_」なしのものを用意
    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<String> = Transformations.map(_price) {
        NumberFormat.getCurrencyInstance().format(it)
    }

    // 外部からアクセスするセッターメソッドも準備する
    fun setQuantity(numberCupcakes: Int) {
        _quantity.value = numberCupcakes
        updatePrice()
    }
    fun setFlavor(desiredFlavor: String) {
        _flavor.value = desiredFlavor
    }
    fun setDate(pickupDate: String) {
        _date.value = pickupDate
        updatePrice()
    }

    // 注文のフレーバーが設定されているかのチェック
    fun hasNoFlavorSet(): Boolean {
        return _flavor.value.isNullOrEmpty()
    }

    // 配達日のリスト(4日分の日付を取得)
    val dateOptions = getPickupOptions()

    // 4日分の日付を返す関数   
    private fun getPickupOptions(): List<String> {
        val options = mutableListOf<String>()

        // 日時のフォーマットを取得。言語はデバイスのロケールを使用
        val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

        // 現在日時を取得
        val calendar = Calendar.getInstance()

        // 4日分の日時を取得
        repeat(4) {
            options.add(formatter.format(calendar.time))
            calendar.add(Calendar.DATE, 1)
        }
        return options
    }

    // 注文のリセット
    fun resetOrder() {
        _quantity.value = 0
        _flavor.value = ""
        _date.value = dateOptions[0]
        _price.value = 0.0
    }

    private fun updatePrice() {
        // elvis演算子の左側の式がnullでない場合はそれを使用し、左側の式がnullの場合は右側の式を使用
         var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
        // 当日配達の割り増し
        if (dateOptions[0] == _date.value) {
            calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
        }
        _price.value = calculatedPrice
    }

    // 初期化処理
    init {
        resetOrder()
    }

}

ナビゲーショングラフ

バックスタックについてはバックスタックを参照

<?xml version="1.0" encoding="utf-8"?>

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/startFragment">
    <fragment
        android:id="@+id/startFragment"
        android:name="com.example.cupcake.StartFragment"
        android:label="@string/app_name"
        tools:layout="@layout/fragment_start" >
        <action
            android:id="@+id/action_startFragment_to_flavorFragment"
            app:destination="@id/flavorFragment" />
    </fragment>
    <fragment
        android:id="@+id/flavorFragment"
        android:name="com.example.cupcake.FlavorFragment"
        android:label="@string/choose_flavor"
        tools:layout="@layout/fragment_flavor" >
        <action
            android:id="@+id/action_flavorFragment_to_pickupFragment"
            app:destination="@id/pickupFragment" />
        <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"
        android:name="com.example.cupcake.PickupFragment"
        android:label="@string/choose_pickup_date"
        tools:layout="@layout/fragment_pickup" >
        <action
            android:id="@+id/action_pickupFragment_to_summaryFragment"
            app:destination="@id/summaryFragment" />
        <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"
        android:name="com.example.cupcake.SummaryFragment"
        android:label="@string/order_summary"
        tools:layout="@layout/fragment_summary" >
        <action
            android:id="@+id/action_summaryFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
</navigation>

xmlレイアウト

通常のビューモデルと共有ビューモデルでは基本的に同じ
データバインディングを使用する場合はlayoutタグで囲み、dataタグ内のvariableタグでnameとtypeを設定

リスナーバインディングとしてonClick属性にメソッド等の記述可能
android:onClick=”@{() -> viewModel.setFlavor(@string/vanilla)}”

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>

<!-- Layout for order summary -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".SummaryFragment">

    <data>

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

        <variable
            name="summaryFragment"
            type="com.example.cupcake.SummaryFragment" />
    </data>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="@dimen/side_margin">

            <!-- Group the order details into a single ViewGroup and set focusable = true,
                 so all fields will be read aloud together by the accessibility service -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:focusable="true"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/quantity_label"
                    style="@style/Widget.Cupcake.TextView.OrderSummary"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/quantity"
                    android:textAllCaps="true"
                    android:textStyle="normal" />

                <TextView
                    android:id="@+id/quantity"
                    style="@style/Widget.Cupcake.TextView.OrderSummary"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="@dimen/order_summary_margin"
                    android:text="@{viewModel.quantity.toString()}"
                    tools:text="6 cupcakes" />

                <View
                    android:id="@+id/divider1"
                    style="@style/Widget.Cupcake.Divider"
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginTop="@dimen/side_margin"
                    android:layout_marginBottom="@dimen/side_margin" />

                <TextView
                    android:id="@+id/flavor_label"
                    style="@style/Widget.Cupcake.TextView.Label"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/flavor" />

                <TextView
                    android:id="@+id/flavor"
                    style="@style/Widget.Cupcake.TextView.OrderSummary"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="@dimen/order_summary_margin"
                    android:text="@{viewModel.flavor}"
                    tools:text="Chocolate" />

                <View
                    android:id="@+id/divider2"
                    style="@style/Widget.Cupcake.Divider"
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginTop="@dimen/side_margin"
                    android:layout_marginBottom="@dimen/side_margin" />

                <TextView
                    android:id="@+id/pickup_label"
                    style="@style/Widget.Cupcake.TextView.Label"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/pickup_date" />

                <TextView
                    android:id="@+id/date"
                    style="@style/Widget.Cupcake.TextView.OrderSummary"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="@dimen/order_summary_margin"
                    android:text="@{viewModel.date}"
                    tools:text="Sunday" />

                <View
                    android:id="@+id/divider3"
                    style="@style/Widget.Cupcake.Divider"
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginTop="@dimen/side_margin"
                    android:layout_marginBottom="@dimen/margin_between_elements" />

                <TextView
                    android:id="@+id/total"
                    style="@style/Widget.Cupcake.TextView.FinalPrice"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:layout_marginTop="@dimen/side_margin"
                    android:text="@{@string/subtotal_price(viewModel.price)}"
                    tools:text="Total $5.00" />

            </LinearLayout>

            <Button
                android:id="@+id/send_button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/side_margin"
                android:onClick="@{() -> summaryFragment.sendOrder()}"
                android:text="@string/send" />

            <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:onClick="@{() -> summaryFragment.cancelOrder()}"
                android:text="@string/cancel" />

        </LinearLayout>
    </ScrollView>
</layout>

アクティビティ

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController

/**
 * Activity for cupcake order flow.
 */
class MainActivity : AppCompatActivity(R.layout.activity_main){

    // ナビゲーションコントローラー
    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ナビゲーション設定
        // activity_main.xmlに定義したFragmentContainerViewを取得
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        // ナビゲーションコントローラーを取得
        navController = navHostFragment.navController
        // アプリバーを表示
        setupActionBarWithNavController(navController)
    }

    override fun onSupportNavigateUp(): Boolean {
        // 上へボタンを有効
        return navController.navigateUp() || super.onSupportNavigateUp()
    }

}

フラグメント

ビューモデルを共有する各フラグメントのクラス変数として共有ビューモデルを定義。
この時、共有ビューモデルの場合はviewModelsではなくactivityViewModels()でデリゲート

onViewCreatedにて共有ビューモデルインスタンスとxmlレイアウトに記載したvariableタグのnameと紐づけ

フラグメント側から値を保存する場合はビューモデルのセッターを利用


import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.cupcake.databinding.FragmentSummaryBinding
import com.example.cupcake.model.OrderViewModel

class SummaryFragment : Fragment() {

    // 共有ビューモデルでのデータ委譲(by activityViewModels())
    // ※単一のフラグメントと紐づくviewModelの場合はviewModels()を
    // デリゲートしていたが、共有ビューモデルの場合はactivityViewModels()
    private val sharedViewModel: OrderViewModel by activityViewModels()

    // バインディングオブジェクトの定義
    private var binding: FragmentSummaryBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // データバインディングの設定
        val fragmentBinding = FragmentSummaryBinding.inflate(inflater, container, false)
        binding = fragmentBinding
        return fragmentBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding?.apply {
            // fragment_summary.xmlで定義したviewModelへの紐づけ
            viewModel = sharedViewModel
            // fragment_summary.xmlで定義したsummaryFragmentへの紐づけ
            summaryFragment = this@SummaryFragment
            // 値の変更を監視
            lifecycleOwner = viewLifecycleOwner
        }
    }

    fun cancelOrder() {
        // 共有ビューモデルの変数を初期化
        sharedViewModel.resetOrder()
        // 初期画面に戻る()
        findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
    }

    // 注文送信処理(注文内容をメールで送信)
    fun sendOrder() {

        // elvis演算子の左側の式がnullでない場合はそれを使用し、左側の式がnullの場合は右側の式を使用
        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)
        }

    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null
    }
}

トレーニング > KOTLIN を用いた ANDROID の基本 > ナビゲーション > ナビゲーションの発展例 > 共有ビューモデル > 4. 共有 ViewModel を作成する