Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。
その中で紹介されているコンポーネントの1つが「LiveData」です。
LiveDataについて、まとめました。
目次
LiveDataとは
LiveDataはデータを格納する箱です。これだけでは変数と変わりません。
しかし、この箱には「データの更新を通知する監視機能」が付いています。
Observerはvalueを監視していて、valueが更新されるとobserveによって登録されたonChange関数を実行(関数オブジェクトへ通知)します。
LiveDataは、ただ単にvalueが更新されたらonChangeを実行するだけで、onChangeが所属するインスタンス(クラス)について関知しません。
LiveDataが効果的な場面
LiveDataが効果的な場面は、UI(User Interface)関連のデータの更新をきっかけにView(TextViewなどのWidget)の表示を更新する時です。
以下のサンプルは、カウンターの値を4桁の数字で表示するアプリです。
private const val KEY_COUNT = "KeyCount" class MainActivity : AppCompatActivity() { lateinit var counter: Counter private val fourFigure: MutableLiveData<String> = // LiveDataの宣言 MutableLiveData<String>("0000") // (コンストラクタで初期化) override fun onCreate(savedInstanceState: Bundle?) { ... // Viewの表示を更新(onChange関数の定義,Observerの登録) fourFigure.observeForever({ fourFigure -> _txtCounter.text = fourFigure }) // Activityの起動で初期値を定義する counter = Counter(savedInstanceState?.getInt(KEY_COUNT, 0)?:0) fourFigure.value = "%04d".format(counter.value) // valueの更新 _btnCount.setOnClickListener { // CountUpボタンのクリックでカウンタを+1にする counter.countUp() fourFigure.value = "%04d".format(counter.value) // valueの更新 } _btnClear.setOnClickListener { // Clearボタンのクリックでカウンタを0にする counter.clear() fourFigure.value = "%04d".format(counter.value) // valueの更新 } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt(KEY_COUNT, counter.value) } }
class Counter(value: Int = 0) { var value: Int = value private set(value) { field = value } fun countUp() { value = if(value == 9999) 0 else ++value } fun clear() { value = 0 } }
LiveData(fourFigure)に格納されたvalueの値を更新するだけで、onChaned関数に定義された「Viewの表示を更新」が実行されます。
LiveDataで特徴的なのは、「データを更新する」というロジック的な部分と「表示を更新する」というUI的な部分が分離できる所です。
―――――――――
次に説明する「LiveDataとViewModelの連携」では、その特徴が一層強まります。
LiveDataとViewModelの連携
LiveDataがもっと効果的な場面は、ViewModelと連携させた時です。
LiveDataをViewModel側へ移動
ViewModelは「 ライフサイクルを超えたデータの引き継ぎ」を行うコンポーネントです。
※詳細は「ViewModelでライフサイクルを超えたデータの引き継ぎ」を参照
LiveDataで表現したUI関連のデータをViewModelインスタンスへ移動させます。これにより、onSave|onRestoreInstanceStateでデータの保存と復元を行う必要はありません。
UI関連のデータをViewModelインスタンスへ移動させたことで、ロジック的な部分とUI的な部分の分離がより一層強まります。
class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by lazy { ViewModelProvider( this, ViewModelProvider.NewInstanceFactory() ).get(MainViewModel::class.java) } private lateinit var fourFigureObserver: Observer<String> override fun onCreate(savedInstanceState: Bundle?) { ... _btnCount.setOnClickListener { viewModel.onClick(it.id) } _btnClear.setOnClickListener { viewModel.onClick(it.id) } fourFigureObserver = Observer<String> { fourFigure -> _txtCounter.text = fourFigure } viewModel.fourFigure.observeForever(fourFigureObserver) // Observerの登録 } override fun onDestroy() { super.onDestroy() viewModel.fourFigure.removeObserver(fourFigureObserver) // Observerの削除 } }
class MainViewModel : ViewModel() { private val counter = Counter() private val _fourFigure: MutableLiveData<String> = MutableLiveData<String>("0000") val fourFigure: LiveData<String> = _fourFigure // LiveData型で外部へ公開 fun onClick(resID: Int) { when(resID) { R.id.btnCount -> { counter.countUp() } R.id.btnClear -> { counter.clear() } else -> {} } _fourFigure.value = "%04d".format(counter.value) } }
このプログラムは注意点が2つあります。
注意点1:インスタンスはLiveData型で公開
LiveDataのインスタンスをMutableLiveData型で公開すると、valueがViewModel以外の場所で変更できてしまいます。
意図的に変更しなければ良いのですが、間違って変更してしまう場合もまります。
よって、外部で変更できないように、LiveData型で公開することが推奨されています。
注意点2:メモリーリークの回避
オブジェクトの寿命は「ViewModel>Activity」になります。よって、ViewModelからActivityを参照する行為はメモリーリークの原因になります。
ViewModel内のLiveDataはonChange関数(関数オブジェクト)でTextViewを参照しています。TextViewは内部にActivity(Context)の参照を持っているので、上記のメモリーリークの条件に適合します。
メモリーリークを回避する方法は、LiveDataに登録されたObserverインスタンス(onChange関数)を削除することです。削除はLiveData#removeObserver( )で行うことが出来ます。
よって、Activityが閉じられる適切なタイミングでremoveObserver( )の実行が必要になります。
―――――――――
次に説明する「LiveDataとLifecycleの連携」では、別の方法でメモリーリークの回避を行います。
LiveDataとLifecycleの連携
LiveDataがもっともっと効果的な場面は、ViewModelならびにLifecycleと連携させた時です。
メモリーリークの回避を自動化
Observer(onChange関数)をLiveDataに登録するobserveには2種類あります。
(1) observeForever(Observer<? super T> observer)
(2) observe(LifecycleOwner owner, Observer<? super T> observer)
(2)はObserverに加えてLifecycleOwnerを引数に持っています。
(2)を使うとObserverはActivityのライフサイクルの状態(Lifecycle.State.XXX)に従って次のような動作になります。
※詳細は「Lifecycleでライフサイクル対応コンポーネント作成」を参照
- INITIALIZED、STARTEDまたはRESUMEDの期間はObserverを登録可能
- STARTEDまたはRESUMEDの期間はvalueの更新を監視
- DESTROYでObserverを自動的に削除
つまり、メモリーリークの回避策である「LiveDataに登録されたObserverインスタンス(onChange関数)を削除する」をActivityの生存範囲に合わせて自動的に行ってくれます。
class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by lazy { ViewModelProvider( this, ViewModelProvider.NewInstanceFactory() ).get(MainViewModel::class.java) } private lateinit var fourFigureObserver: Observer<String> override fun onCreate(savedInstanceState: Bundle?) { ... _btnCount.setOnClickListener { viewModel.onClick(it.id) } _btnClear.setOnClickListener { viewModel.onClick(it.id) } viewModel.fourFigure.observe(this, { fourFigure -> // Observerの登録 _txtCounter.text = fourFigure }) } }
LiveData#observe( )を使えばメモリーリークの心配は無くなります。
補足1:LiveDataへ複数のObserverを登録
LiveDataへ複数のObserver(onChange関数)を登録できます。
実行は登録した順番です。
fourFigure.observe(this, { fourFigure -> Log.i(TAG, "(1)onChangeが実行されました! fourFigure = ${fourFigure}") }) fourFigure.observe(this, { fourFigure -> Log.i(TAG, "(2)onChangeが実行されました! fourFigure = ${fourFigure}") }) fourFigure.observe(this, { fourFigure -> Log.i(TAG, "(3)onChangeが実行されました! fourFigure = ${fourFigure}") }
: I/MainActivity: (1)onChangeが実行されました! fourFigure = 0003 I/MainActivity: (2)onChangeが実行されました! fourFigure = 0003 I/MainActivity: (3)onChangeが実行されました! fourFigure = 0003 :
補足2:SAM変換の例
LiveData#observeForeverならびに#observeの引数ObserverはSAM形式です。
※SAM:Single Absorute Method、抽象メソッドが一つの抽象クラス
public interface Observer<T> { void onChanged(T t); }
よって、SAM変換されることを狙って、引数をObserverのインスタンスからラムダ式(関数オブジェクト)へ置き換えることが出来ます。
※詳細は「Kotlin:SAM変換」を参照
ラムダ式は省略の方法が多彩なので例を示しておきます。
// ★関数オブジェクトの参照値を受け渡す // 省略できる箇所なし fun onChange(fourFigure: String) { _txtCounter.text = fourFigure } fourFigure.observe(this, ::onChange) // ★無名関数を受け渡す // 省略できる箇所なし fourFigure.observe(this, fun(fourFigure: String){ _txtCounter.text = fourFigure }) // ★ラムダ式(無名関数)を受け渡す // 原形 fourFigure.observe(this, { fourFigure: String -> _txtCounter.text = fourFigure }) // 引数リストの最後がラムダ式ならカッコの外に出せる fourFigure.observe(this) { fourFigure: String -> _txtCounter.text = fourFigure } // ラムダ式の引数の型が明確な場合(ここではString)は削除可能 fourFigure.observe(this) { fourFigure -> _txtCounter.text = fourFigure } // ラムダ式の引数が一つの場合は削除可能、引数はitで参照 fourFigure.observe(this) { _txtCounter.text = it }
関連記事: