カテゴリー
Kotlin言語

Androidアプリ:ViewModel と LiveData + オブザーバ でUIとデータを管理(Kotlin編)

Android Developers > スタートガイド > Kotlin と Android をゼロから学ぶ > Unit3 – Architecture の内容を中心にまとめたものです(翻訳ではありません)


アーキテクチャとは

アプリに機能を追加してたら、コードがめちゃくちゃになってきたような…

そういうときは Architecture Components を使うようにするといいの

  • 強固で柔軟で保守しやすいアプリにするには、アプリを一定のアーキテクチャ(アプリの構造や設計の指針をまとめたもの)に従って作成していく必要があります
  • Android Jetpack ライブラリにあるAndroid Architecture Components を利用すると、適切なアーキテクチャに基づいたアプリを簡単に作成できます
  • Android の Architecture の主要な部分は次の4つです
    • UI Controller(activity/fragment のこと)
    • ViewModel
    • LiveData
    • Room
  • ここでは ViewModel, LiveData についてまとめます
  • なお、Architecture の基本的な構造は次のようになります
UI Controller(activity/fragment)・・・データ表示・ユーザイベント取得
          ↓
ViewModel・・・UIに必要とされる全データを保持・画面表示用にそれを整える

アーキテクチャの原則

  • 最も一般的なアーキテクチャにおける原則( architectural principles )は関心の分離(separation of concerns)とモデルからUIを追い出すこと(drive UI from a model)です
  • 関心の分離(separation of concerns)」設計原則は、アプリはクラスに分けられ、それぞれが分離した役割を持つものであるべき、という考え方です
  • もうひとつの重要な原則は「モデルからUIを追い出すこと(drive UI from a model)」です。モデルからはUIに関することを一切排除して、できるだけ永続的なモデルにすべき、という考え方です

次のような問題を解決します

UI関係のごちゃごちゃ、更新関係のごちゃごちゃ、をすっきりできちゃいます

  • コンフィグチェンジ(デバイスを回転させたときなどに発生)などでAndroidシステムによってUI画面が再生成されても、アプリのデータに影響が及ばない(データが保持され続ける)ようにする
  • アプリのデータが不用意に書き換えられたり、壊されたりしないようにする
  • データの更新を監視してUIの表示を更新する(データの更新処理とUIの更新処理を分離する)

次のような方法を使います

ViewModel と LiveData の使い方を覚えることになりますね

  • アプリのUI画面に関することと、UIに関係しないロジックの部分をしっかり分離する方法・・ViewModel クラスとプロパティデリゲート(委譲プロパティ)を使います
  • データの書き換え管理をしっかり行う方法・・・バッキングプロパティ(backing property)を使います
  • データの更新を検知して、UI部品の表示が更新されるようなKotlinコードを記述する方法・・・LiveData と オブザーバ を使います
  • データの更新があったときに、レイアウトが自動的にUI部品の表示を更新する方法・・・LiveData と Data Binding を使います  LiveData と Data Binding についてはこちら 

UI controller (Activity / Fragment) とは

アクティビティやフラグメントには、UI を扱うお仕事だけ記述してくださいね

  • UI controller はUIをコントロールするものです。通常、アクティビティやフラグメントが UI controller になります
  • 画面上のUI部品(view)を描画したり、ユーザイベントを取得したり、ユーザが操作するUIに関するその他すべてのことを担当します
  • アプリのデータや、そのデータを使った処理の部分は UI controller クラスに含めないようにします
  • Android では、デバイスを回転させたり、メモリ不足になったりすると、システムによって UI controller が破壊されます
  • アプリのデータなどを UI controller に保存するとそれらも一緒に破壊されてしまいます
  • データやそれを処理するメソッドなどをすべて ViewModel に入れて管理することで UI controller の影響を受けないようにします

ViewModel とは

ViewModel って、アプリデータの保管庫みたいなかんじかも…

  • UI部品(view)に表示されるデータはすべて ViewModel で扱うようにします
  • ViewModel を使うことでアーキテクチャの原理に従う形でモデルからUIを分離できます
  • ViewModel にあるデータはAndroidシステムによってアクティビティやフラグメントが破壊されたり、再生成されたりしても保持されます
  • ViewModel に保持されたデータは、再生成されたアクティビティやフラグメントのインスタンスで利用できます
  • アプリには ViewModel クラスを継承して実装します
  • ViewModel からUI部品(View)へは一切アクセスしないようにします。アクティビティ、フラグメント、View Binding オブジェクトへのアクセスも同様です
  • ViewModel の利用は以下のようにします。ViewModel はアクティビティやフラグメントからアクセスされる側になります
 MainActivity → Fragment → ViewModel

ViewModel からフラグメントとかにアクセスしちゃだめなんですね

onSaveInstanceState() との違い

コンフィグチェンジ(デバイスの方向を変えたりしたときに発生)などがあってもアプリのデータなどが維持されるようにするには onSaveInstanceState() コールバックを使う方法もあります。ただし次のような問題があります

  • onSaveInstanceState() メソッドを使うには、バンドルに状態を保存するコードを書く必要があります
  • 保存したデータを復元するしくみも実装する必要があります
  • 保存できるデータ量が小さいことにも注意します

ViewModel を利用するための設定

build.gradle へ設定をしてくださいね

  • ViewModeが使えるように設定をします
  • Android Studio でモジュールレベルの build.gradle ファイルを開きます
  • dependencies ブロックの中に次の行を書き込んで Sync すると ViewModel ライブラリが使えるようになります
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

ライブラリは最新版( latest version )を使うようにします

ViewModel クラスを作成する

ViewModel クラスを継承するだけで作れちゃう

  • SampleViewModel という名前で Kotlin クラスファイルを作成します(フォルダで右クリック > New > Kotlin File/Class)
  • SampleViewModel という名前を入れて、リストから Class を選択して作成します
  • 以下のように SampleViewModel を ViewModel のサブクラスになるように書き換えます(ViewModel は抽象クラスなので、使用するには必ず継承して実装します)
class SampleViewModel : ViewModel() {
}

この中にアプリで利用するプロパティやメソッドを追加していきます

ViewModel と UI controller を結び付ける

利用する方(UI controller)で設定するの

by viewModels() っていうのが大事っぽい

  • ViewModel を UI controller(アクティビティ/フラグメント)に関連付けるのに、UI controller 内に ViewModel への参照(オブジェクト)を作成します
  • UI controller である SampleFragment クラス内に次のようにプロパティを宣言します
private val viewModel: SampleViewModel by viewModels()
  • SampleViewModel 型のプロパティ viewModel を宣言しています
  • by viewModels() をつけることで「プロパティデリゲート(委譲プロパティ)」として宣言できます
  • プロパティデリゲートによって、SampleViewModel にあるすべての変数が UI controller(SampleFragment)からライフサイクルに影響されずに参照できるようになります。
  • Android システムによって SampleFragment が破壊され、再生成された後でも、SampleViewModel に保存されている同じ値が参照されるようになります
  • 尚、Android Studio の自動インポートが有効でない場合は import androidx.fragment.app.viewModels とします

プロパティデリゲート(property delegate, 委譲プロパティ)とは

プロパティデリゲートを使うと、値(データ)の管理を他のクラスに丸投げできちゃうの

ViewModel にアプリのデータを管理してもらうのに、これを使うんですね

  • Kotlin では、mutable(変更可能)なプロパティ(var)にデフォルトの getter/setter 関数が用意されます
    • getter/setter 関数は、プロパティの値を取得したり設定したりするときに呼ばれます
  • 読み込み専用(read-only)プロパティ(val)には getter 関数のみがデフォルトで生成されます
  • プロパティデリゲート(委譲プロパティ)を使うと、プロパティの getter-setter の役割が他のクラス(デリゲートクラス)へ渡されます
  • つまり、実際の値の管理はデリゲートクラス(delegate class)が行うようになります
  • プロパティデリゲートは「by デリゲートクラスのインスタンス」という形で宣言します
  • プロパティデリゲートを使うと、UI controller が破壊、再生成されて ViewModel を参照する新しいインスタンス(viewModel)が生成されても、保持されているデリゲートクラスのオブジェクトを参照するようになります
  • デリゲートクラスは最初にアクセスしたときに viewModel オブジェクトを生成します。コンフィグチェンジの間もその値を保持し、要求されたときに値を返します
  • by viewModels() の viewModels は、あらかじめ用意されているデリゲートクラスです
    • ViewModel でプロパティデリゲートを利用するときにはこれを使えばよく、独自に用意する必要がありません
    • アクティビティに関連付ける activityViewModels を使うと、フラグメント同士で同じ ViewModel(のインスタンス)を共有できます

データの預け先は、あらかじめ用意されている viewModels や activityViewModels を使えばいいんですね

ViewModel にバッキングプロパティを追加する

クラスのプロパティを、外部から直接書き換えられないようにするの

うっかり変な値を入れちゃったりしなくなりそう

  • ViewModel のプロパティは、クラス外部から勝手に書き換えられないように参照のみ可能にするのが望ましいです
  • しかし、クラス内部からは自由に書き換え可能にしておかないと値が変更できなくなってしまいます
  • つまり同じ変数を、クラスの外部向けには public val で宣言、クラスの内部向けには private var で宣言するのが理想的です
  • バッキングプロパティ(backing property)を使うことでこれを実現します
  • バッキングプロパティは次のように宣言します
private var _sampleData = "test"
val sampleData: String
   get() = _sampleData
  • クラスの内部では _sampleData を使います
  • クラスの外部からは sampleData を使います。sampleData を参照すると _sampleData の値が返されるようになっています

ポイントをまとめますね

  • バッキングプロパティは、同じ値を扱うのに、内部向けの _sampleData と外部向けの sampleData に二重化して値の安全性を確保する手法です
  • ViewModel の変更可能なデータフィールドは公開しないようにします
  • ViewModelの内部向けの変更可能なデータは常に private とすべきです
  • ViewModel のデータが他のクラスから直接変更できないことを確認しましょう

ViewModel のプロパティの値を変更する

プロパティの値の書き換えには、書き換え用のメソッドを使うんですね

  • ViewModel のプロパティの値を変更できるように、変更のためのメソッド(ヘルパーメソッド)を用意します
  • 外部のクラスは、ViewModel のヘルパーメソッドを呼び出すことで ViewModel の内部向けプロパティを変更します
  • 他にも、ViewModel 内のデータ利用する処理を行う場合にもヘルパーメソッドを作成して利用します

ViewModel のライフサイクル

  • Android のフレームワークは、アクティビティやフラグメントのスコープが生きている限り、ViewModel を生かし続けます
  • コンフィグチェンジなどによってアクティビティやフラグメントが破壊されても、ViewModel は破壊されません
  • 再生成されたアクティビティやフラグメントの新しいインスタンスは、破壊されずに残っている ViewModel のインスタンスに再接続されます
  • 下の図のようになります

値を ViewModel に入れておけば、途中でいつのまにか初期化されたりする心配がないんです

Activity createdonCreateViewModelViewModel created
onStart
onResume
Activity rotated
onPause
onStop
onDestroy
New Activity instance created
onCreate
onStart
onResume
finish() called from code
onPause
onStop
onDestroy
Activity finishedonCleared()ViewModel destroyed

ViewModel のライフサイクルを確認する

Kotlin コードに記述した Log.d() からの出力を、Android Studio の Logcat で確認してくださいね

init ブロックの利用

  • Kotlin には初期化ブロック(initブロックとしても知られている)があります。オブジェクトインスタンスを初期化するときに必要とされるコードはここに記述します
  • 初期化ブロックは init { ・・・ } のような形をしています
  • 初期化ブロックはオブジェクトインスタンスが最初に生成されて初期化されるときに実行されます
  • ViewModel が生成されるタイミングでログ出力させたいときはここに命令を記述します

onCleared() メソッドの利用

  • ViewModel は、関連付けられたフラグメントが切り離されたり、アクティビティが終了したりすると破棄されます
  • ViewModel が破棄される直前に onCleared() コールバックが呼ばれます
  • 実装した ViewModel クラスの中で、onCleared() メソッドをオーバーライドします
  • ViewModel が破棄されるタイミングでログ出力させたいときはonCleared() の中に命令を記述します

LiveData とは

変数(プロパティ)を LiveData にすると、変数の値が変わったら検知できるようになるの

  • ViewModel はアプリの中に用意するデータの保存場所です
  • LiveData は、変数に保存されている値が変化したことを検知できるクラス(observable data holder class)です
  • LiveData はライフサイクルを考慮して動作します
  • ViewModel に作成したプロパティに LiveData を適用して、値の変化を検知できるようにします
  • 値の変化を検知してUI画面の更新を行うことで、値を更新する処理と、UI画面を更新する処理を分離することができます

LiveData について学ぶこと

書き換えできるのと、できないのがあるんですね

  • LiveData 型と MutableLiveData 型の使い方
  • LiveData を使って ViewModel の中に蓄えられたデータをカプセル化する方法
  • LiveData における変更を監視(オブザーブ)するためのオブザーバメソッドを追加する方法

LiveData の特徴

どんな型の変数でも、LiveData にすることができるの

LiveData はライフサイクル対応(lifecycle-aware)の監視可能なデータ保持クラス(オブザーバブルなデータホルダークラスobservable data holder class)です

  • LiveData はデータを保持します
  • LiveData はどんなタイプのデータでも利用できるラッパーです
  • LiveData は監視可能(オブザーバブル)です
  • それはLiveDataオブジェクトによって保持されているデータが変更したとき、オブザーバはそれを通知されることを意味します
  • LiveData はライフサイクル対応(lifecycle-aware)です
  • LiveDataにオブザーバを取り付ける(アタッチする)と、オブザーバは LifecycleOwner(通常はアクティビティかフラグメント)と関連付けられます
  • LiveData だけが STARTED や RESUMED のようなアクティブなライフサイクル状態にあるオブザーバを更新します
  • つまりオブザーバは、変数の値がライフサイクルオーナー、つまりアクティビティやフラグメントの存続期間(ライフタイム)の間、ViewModelの内部で変更があるとき、発動されます
  • LiveDataやオブザベーションについて、詳しくは こちら をどうぞ

MutableLiveData とは

  • MutableLiveData は LiveData の変更可能(mutable)バージョンです
  • MutableLiveData の中に保存されたデータの値は変更することができます

たとえば、こんな風にすれば LiveData なプロパティでも、バッキングプロパティにできちゃうの

private val _sampleData = MutableLiveData<String>()
val sampleData: LiveData<String>
   get() = _sampleData
  • SampleViewModel の中で、変数 _sampleData の型を MutableLiveData<String> にします
  • LiveData と MutableLiveData はジェネリクス(ジェネリッククラス)です。なので保持するデータ型を指定する必要があります
  • _sampleData の変数型は val とします

val にしちゃうと変更できなくなっちゃうような…

ううん、MutableLiveData オブジェクト自体はそのままでいいの。オブジェクトの中に保存されているデータのみが変更されるの

“Mutable” って、オブジェクトの中の値が「変更可能」って意味なんですね

LiveData 型の変数の値を取得する

LiveData に保存されている値には value プロパティを使ってアクセスしてくださいね

LiveData オブジェクトの中のデータにアクセスするには、value プロパティを使います

_sampleData.value = "test"

LiveData オブジェクトにオブザーバを設定する

LiveData のいちばんのポイントはここ、値が変わったことを検知できるの

observe() っていうメソッドを使えばいいのかな?

うん、ボタンに click listener を設定するときと同じように、LiveData な変数に observe を設定してあげればいいの

  • UI controller(アクティビティ,フラグメント)のなかで、ViewModel クラスのインスタンス viewModel 中の LiveData型プロパティ sampleData にオブザーバを設置します
  • 追加するオブザーバはアプリのデータ sampleData への変更を監視(オブザーブ)します
  • LiveData はライフサイクル対応です。LiveData だけがアクティブなライフサイクルの状態にあるオブザーバを更新できます
  • 例えば、フラグメントにあるオブザーバは、フラグメントが STARTED あるいは RESUMED の状態にあるときにのみ通知されます
  • オブザーバの設定は、sampleData で observe() メソッドを呼び出すことで行います
viewModel.sampleData.observe(viewLifecycleOwner,
   { value ->
       binding.textView.text = value
   })

または

viewModel.sampleData.observe(viewLifecycleOwner) { 
       binding.textView.text = it
}
  • ラムダ式は宣言されていない無名関数です。式として直接渡されます
  • ラムダ式は中括弧 { } で囲まれています
  • sampleData が更新されると、それを検知してテキストビューに表示されている sampleData の値が更新されます

後のほうの書き方は trailing lambda syntax(後置ラムダ構文)って言うそうです