#この記事は
お寿司屋で例えるAndroid Architecture Compoment 第二貫:DataBindingの続きです。
前回は、Android Architecture CompomentのData Bindingについて解説しました。
####これまでのシリーズを見てない人へ
一応共有しておきたいことで言うとこちら。
View(Activity/Fragment)はお客さん
ViewModelが板前さん
Repositoryが、漁師
Modelが、海
MVVMを海からお魚を取ってきてお客さんに提供するまでの役割を分割したものと捉えています。
詳しくは、第一貫をご覧ください。(5分位で読める内容ですよ!)
#本シリーズで作成するアプリ
今回このシリーズを通して実装する事は、お寿司屋で例えるAndroid Architecture Compoment ということで、アプリ自体も先ほど共有したお寿司屋さんの流れを踏襲します。
こんな感じのアプリ作りまーす!
マグロしか食べてなかったり、小学生女子が下校途中にお寿司屋に寄っていたりとツッコミどころがありますが、スルーしてください。
また、見た目はくそアプリですが、設計はMVVMで作ります。(この時点では、View-ViewModel間しか実装してませんが…)
先程のMVVMの例えと、アプリの例えが少し違いがありますが、こちらもスルーで。
###View(お客さん)
注文画面を表示
###ViewModel(板前さん)
注文画面のデータや処理を保持
###Model
注文履歴を保持
あ、ここ違くね???ってところがありましたら、コメントか筆者のTwitterにて教えていただけると助かります。
#本題
今回は、LiveDataについて解説していきます。
###LiveDataとは?
ドキュメントによると
LiveData は監視可能なデータホルダー クラスです。
LiveDataの役割は、「通知センサ」です。
お寿司屋で例えると、某回転寿司チェーンではお馴染みの「タッチパネル」では、お客さんが注文したお寿司が来ると通知によって教えてくれますよね!
それと同じ感じ!それをLiveData、及びObserverが担います。
ちなみに、LiveDataが通知センサの内の「送信」、Observerが「受信」を担います。
もう少し、ドキュメントを読んでみましょう。
通常の監視とは異なり、LiveData はライフサイクルに応じた監視が可能です。つまり、アクティビティ、フラグメント、サービスなどの他のアプリ コンポーネントのライフサイクルが考慮されます。このため、LiveData はライフサイクルの状態がアクティブなアプリ コンポーネント オブザーバーのみを更新します。
訳)お客さん(=アクティビティ、フラグメント、サービスなどの他のアプリ コンポーネント)が、注文し始めた時に通知センサーを起動させた方が無駄がなくて良い。
こんな感じのことを言ってますw
LiveData を使用するメリット
LiveData を使用するメリットには次のようなものがあります。
UI をデータの状態と一致させることができる
LiveData はオブザーバーのパターンに従います。LiveData は、基盤となるデータが変更されると Observer オブジェクトに通知します。Observer オブジェクト内の UI を更新するコードは統合できます。これにより、オブザーバーが自動的に更新を行うので、アプリデータが変更されるたびに UI を更新する必要がなくなります。
訳)板前さんがお寿司を握ると(=基盤となるデータが変更されると)、自動でお客さんに通知がいきます(=Observer オブジェクトに通知します)。
メモリリークが発生しない
オブザーバーは Lifecycle オブジェクトにバインドしていて、関連付けられているライフサイクルが破棄されたときに自身のクリーンアップを行います。
停止されたアクティビティに起因するクラッシュが発生しない。
オブザーバーのライフサイクルがアクティブでない場合(アクティビティがバックスタック内にある場合など)、オブザーバーは LiveData イベントを受け取りません。
メモリリークとは、メモリの解放忘れで空き容量が減ることです。
通知センサをずっと起動したままだと、電力が無駄になります(≒解放わすれ)。
その為、通知センサはお客さんが来たら起動し、お客さんが帰ると停止する方が効率がいいよねって感じのことを言ってます。
手動によるライフサイクル処理が不要
UI コンポーネントは関連データを監視するだけで、監視の停止や再開は行いませんが、 LiveData はそのすべてを自動的に管理します。これは、LiveData が監視を行いながら、関連するライフサイクルの状態の変化を認識するためです。
先程の似たようなことですが、お寿司語で訳すと、通知センサは機械で自動でオンオフをしてくれるので、手動で電源を操作する必要ないよねってことを言ってます。
大体LiveDataがどんなものか分かってきたでしょうか?
早速、簡単な実装に移ってみます。
###導入
以下の依存関係を追加します。
まずは、ライフサイクルのバージョン設定。
buildscript {
ext {
lifecycle_version = '2.1.0'
}
}
そして、ViewModel及びLiveDataの追加。
dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
}
###作ってみる
LiveData オブジェクトを操作する手順は次のとおりです。
1.特定のタイプのデータを保持する LiveData のインスタンスを作成します。これは通常、ViewModel クラス内で行います。
訳)板前さんが、通知センサ(ViewModel)の主導権を握ります。
まずは、ViewModelを継承させた板前さん(OrderViewModel)を作成します。
2.onChanged() メソッドを定義する Observer オブジェクトを作成します。このオブジェクトは、LiveData オブジェクトが保持するデータが変更されたときの処理を管理します。通常は、UI コントローラ(アクティビティやフラグメントなど)内に Observer オブジェクトを作成します。
3.observe() メソッドを使用して、Observer オブジェクトを LiveData オブジェクトにアタッチします。observe() メソッドは LifecycleOwner オブジェクトを取得します。これにより、Observer オブジェクトが LiveData オブジェクトに登録され、変更が通知されるようになります。通常は、UI コントローラ(アクティビティやフラグメントなど)内の Observer オブジェクトをアタッチします。
特定のタイプのデータを保持する LiveData のインスタンスを作成します。これは通常、ViewModel クラス内で行います。
いつもの通りドキュメントは、かなりお堅い日本語を使われているので、しつかりとお寿司屋に例えて解説していきたいと思います!
####板前さん側
class OrderViewModel : ViewModel() {
// 画像のidを保持
private val _orderImage = MutableLiveData<Int>()
val orderImage: MutableLiveData<Int>
get() = _orderImage
// 注文ボタンのテキストの状態を保持
private val _orderText = MutableLiveData<String>()
val orderText: LiveData<String>
get() = _orderText
// 会計ボタンのテキストの状態を保持
private val _billText = MutableLiveData<String>()
val billText: LiveData<String>
get() = _billText
private val _customerType = MutableLiveData(CustomerType.ENTER)
// 注文ボタンのテキストの状態だけ初期値を設定
init {
_orderText.value = "入店"
_orderImage.value = R.drawable.sushi_syokunin_man_mask
}
// お客さんの行動
fun orderTuna() {
when (_customerType.value) {
CustomerType.ENTER, CustomerType.RE_ENTER -> {
_orderImage.value = R.drawable.sushi_syokunin_man_mask
_orderText.value = "マグロ"
_billText.value = "お会計"
_customerType.value = CustomerType.EAT_TUNA
}
CustomerType.EAT_TUNA -> {
_orderImage.value = R.drawable.sushi_akami
_orderText.value = "完食"
_customerType.value = CustomerType.COMPLETED_EAT
}
CustomerType.COMPLETED_EAT -> {
_orderImage.value = R.drawable.sushi_syokunin_man_mask
_orderText.value = "マグロ"
_customerType.value = CustomerType.EAT_TUNA
}
CustomerType.GO_HOME -> {
viewModelScope.launch {
_orderImage.value = R.drawable.tsugaku
_orderText.value = ""
delay(1000)
_orderImage.value = R.drawable.tsugaku
_orderText.value = "再入店"
_customerType.value = CustomerType.RE_ENTER
}
}
}
}
// お会計ボタンを押したときの処理
fun pay() {
when (_billText.value) {
"お会計" -> {
_orderImage.value = R.drawable.message_okaikei_ohitori
_orderText.value = "帰る"
_billText.value = ""
_customerType.value = CustomerType.GO_HOME
}
}
}
enum class CustomerType {
ENTER,
RE_ENTER,
EAT_TUNA,
COMPLETED_EAT,
GO_HOME
}
}
前々回の記事:どんなもんか知るにて、依存関係はView->ViewModel->Modelの方向にしかないと説明しました。
つまり、お客さん(Fragment)は板前さん(ViewModel)に依存しています。
その為、お客さん(Fragment)の注文やお会計に関するデータや処理は、全て板前さん(ViewModel)が「握っています」。
######板前だけにな
失礼しました。
そのようなイメージが分かると良いです。
ちなみに、MutableLiveDataとLiveData二つありますが、違いは変更できるか否かです。
Mutableは、日本語で可変という意味なのでそこから推測できた人もいるかもしれません。
では、何故この二つが必要なのか?
オブジェクト指向の考え方で、カプセル化というものがあります。
これは、簡単に言うと外部から変更できないように「隠す」ことです。
この場合、OrderViewModelが保持している注文をOrderFragmentで変更出来てしまったらまずいんです。
お客さんが、注文メニューを変更できちゃうくらいやばいことです。
可愛そうなことにお店は、マグロしか取り扱っていないようなので、サーモンに書き換えられたら店を閉めるしかありません…
前半の書き方の意味はこんな感じです。
では、次。
init { }。
これは、データを初期化しているだけです。
ん-、fun以降も関数を宣言しているだけなので、OrderViewModelは、これ以上特に説明は要らないかなと思います。
ちなみに、MutableLiveDataにデータをセットしたり、また取得したりするときは、valueというメソッドを使います。
####お客さん側
class OrderFragment : Fragment() {
// 客席にタッチパネル設置
private val viewModel: OrderViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = FragmentOrderBinding.inflate(inflater, container, false).let {
it.lifecycleOwner = viewLifecycleOwner
it.orderButton.setOnClickListener { viewModel.orderTuna() }
it.billButton.setOnClickListener { viewModel.pay() }
// 監視センサ作成
val imageObserver = Observer<Int> { newImageId ->
// ImageViewの更新処理
it.orderImageView.setImageResource(newImageId)
}
// 監視センサ作成
val textObserver = Observer<String> { newText ->
// ボタンテキストの更新処理
it.orderButton.text = newText
}
val billObserver = Observer<String> { newText ->
it.billButton.text = newText
}
// viewLifecycleOwnerを第一引数を渡すことで、お客さんに合わせて監視する設定
viewModel.orderImage.observe(viewLifecycleOwner, imageObserver)
viewModel.orderText.observe(viewLifecycleOwner, textObserver)
viewModel.billText.observe(viewLifecycleOwner, billObserver)
it.root
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/orderImageView"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_marginTop="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/orderButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/orderImageView" />
<Button
android:id="@+id/billButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/orderButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
まずは、先頭のviewModelの宣言。 by viewModels()という委譲を付けることでライフライクルを同期することが出来ます。
setOnClickeListnerに関しては、viewModelで宣言した関数を使っています。
これによって、xml側で宣言することなく使えます。
また、依存関係の方向がしっかりとお客さん->板前さんになっています。
監視センサを作成している部分に触れます。
お客さんは、通知を受信する側なのでObeserverを実装します。
3つのViewは、セットする値が違いますが、同じことを書いています。
例えば、ImageViewに関して、it.orderImageView.setImageResource(newImageId)とすることで、板前さん側で画像のリソースIDが変更された時、自動的にこちらに通知がやってきてUIを変更してくれます。
そして、最後のobserveメソッドの部分。
引数に、viewLifecycleOwnerと上で作成したobserverを持ちます。
第一引数のviewLifecycleOwnerは、ライフサイクルの同期を実現します。
お客さんが注文してから、会計する間だけ受信センサを起動させることで無駄がなくなります。
ソフトウェア的には、メモリリークを回避できます。
解説としては、以上になります。
###第四貫へ続く