LiveDataでデータの更新を通知

投稿日:  更新日:

Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。

その中で紹介されているコンポーネントの1つが「LiveData」です。

LiveDataについて、まとめました。

スポンサーリンク

LiveDataとは

LiveDataはデータを格納する箱です。これだけでは変数と変わりません。

しかし、この箱には「データの更新を通知する監視機能」が付いています。

LiveDataの構成

Observerはvalueを監視していて、valueが更新されるとobserveによって登録されたonChange関数を実行(関数オブジェクトへ通知)します。

LiveDataは、ただ単にvalueが更新されたらonChangeを実行するだけで、onChangeが所属するインスタンス(クラス)について関知しません。

スポンサーリンク

LiveDataが効果的な場面

LiveDataが効果的な場面は、UI(User Interface)関連のデータの更新をきっかけにView(TextViewなどのWidget)の表示を更新する時です。

以下のサンプルは、カウンターの値を4桁の数字で表示するアプリです。

Viewの表示を更新

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 }
スポンサーリンク

関連記事:

Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 有効そうだけど、実態がよくわからないので、いろいろ調べて理解した内容をまとめました。 ...
Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 その中で紹介されているコンポーネントの1つが「Lifecycle」です。 Lifecycleについて、まとめました。 ...
Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 その中で紹介されているコンポーネントの1つが「ViewModel」です。 ViewModelについて、まとめました。 ...
Androidの開発スピードが速過ぎます。個人で習得を進めている私には、追い付いて行けません。 これが会社などであれば、グループ内のメンバーで分担して習得し、後に共有、などといった対応が可能でしょう。組織の強みですね! 先日も、久しぶりにAndroid Studioで新規プロジェクトを作成したら、Jetpack Composeのプロジェクトになってました。「はて?、これはどうすれば良いのだ?」と、頭の中は疑問符だらけで、プログラミングが先へ進めませんでした。 Jetpackの存在は知りながら、“使わなくてもアプリは作れる”と、学習は後回していたからです。 プロジェクトのひな型にJetpack Composeが採用されたならば、今後はJetpackの利用が開発の主軸になっていくのでしょう。 遅れ馳せならが、Jetpackの重要性を知った次第です。 ここで本腰をいれて習得しないと、さらに後方へ置いて行かれそうです。 という訳で、Jetpackの学習を始めました。 今回はJetpackそのものについてまとめます。 ※環境:Android Studio Flamingo | 2022.2.1 ...
スポンサーリンク