共有ビューモデル
基本的にはフラグメント個別のビューモデルと共有ビューモデルの内容は同じ
複数のフラグメント間でデータやメソッドを共有する。
基本的にはフラグメント個別のビューモデルと共有ビューモデルの内容は同じ
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 を作成する