しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

【Kotlin】ViewModelとLiveDataでUIの更新を楽にする

ViewModelとLiveDataを試してみました。
ViewModelとLiveDataは個別に使うこともできるのですが、セットで使われる事が多いようなので一緒に試してみました。

ViewModelを利用する

まずはViewModelを使うためにモジュールのbuild.gradleに下の1行を追加します。

implementation "androidx.activity:activity-ktx:1.4.0"

ViewModelは下のように定義します。

class MyViewModel: ViewModel() {
    var test = 0
}

利用方法は下の通りです。
ActivityのviewModelsに委譲する形で変数を定義します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val vm: MyViewModel by viewModels()
        Log.d("Test", "${vm.test}") // → 0
        vm.test = 1
        Log.d("Test", "${vm.test}") // → 1
        val vm2: MyViewModel by viewModels()
        Log.d("Test", "${vm2.test}") // → 1
    }
}

viewModelsはActivityの保持するプロパティーなので、下コードのvmとvm2の持つプロパティーは共通になります。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val vm: MyViewModel by viewModels()
        Log.d("Test", "${vm.test}") // → 0
        vm.test = 1
        val vm2: MyViewModel by viewModels()
        Log.d("Test", "${vm2.test}") // → 1
    }
}

LiveDataを利用する

LiveDataを使うと値の変更があった時、自動的に画面の表示も更新できるようになります。
まずはBindingを使うためにbuild.gradleでBindingを有効にします。

android {
    ...
    dataBinding {
        enabled = true
    }
}

次はLiveDataを使い、下のようにViewModelを定義します。

class MyViewModel: ViewModel() {
    val myProperty = MutableLiveData("test")
}

レイアウトファイルでViewModelの変数を使う方法は下の通りです。
variableタグで変数を定義して@{}の内部でそのプロパティーにアクセスしています。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="vm" type="com.example.myapp.MyViewModel"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.myProperty}" />
    </LinearLayout>
</layout>

xmlへViewModelを渡す処理は下の通りです。
lifecycleOwnerがないとLiveDataがうまく動かないので注意が必要です。

class MainActivity : AppCompatActivity() {
    val vm: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
            this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.vm = vm
    }
}

起動すると画面にmyPropertyの値が表示できています。

f:id:llcc:20220227154344p:plain

LiveDataの値は下のように書き換えます。 書き換えるとViewも即時更新されます。

vm.myProperty.value = "test2"

ビュー側の変更をViewModelに反映するには@{}ではなく@={}を利用します。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="vm" type="com.example.myapp.MyViewModel"/>
    </data>

    <LinearLayout
        android:id="@+id/content_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={vm.myProperty}" />
    </LinearLayout>
</layout>

Activity側でのプロパティー変更の監視方法は下の通りです。
myPropertyの値が変更された時に下の処理が呼ばれます。

vm.myProperty.observe(this) {
    Log.d("Test", it)
}

別のプロパティーに依存するLiveDataの定義ではMediatorLiveDataを使います。
例えば入力した文字数をリアルタイムで表示したい場合、下のようにtextCountを監視します。

ここで利用しているaddSourceメソッドはobserveと違い引数にLifecycleOwnerが不要です。
そのため下のようにViewModel内で処理を完結する事ができます。

class MyViewModel: ViewModel() {
    val myProperty = MutableLiveData("test")
    val textCount = MediatorLiveData<Int>()

    init {
        val observer = Observer<String> {
            textCount.value = myProperty.value?.count() ?: 0
        }
        textCount.addSource(myProperty, observer)
    }
}

レイアウトは下のように記述します。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="vm" type="com.example.myapp.MyViewModel"/>
    </data>

    <LinearLayout
        android:id="@+id/content_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={vm.myProperty}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.textCount.toString()}" />
    </LinearLayout>
</layout>