Android Kotlin の ViewModel で Model の変更を受け取る -1
今回は珍しく長いです。
先日から Android のタスクにまた取り組んでいます。リハビリをかねて Jetpack compose を勉強しています。
自分の理解では、@composable な関数は外から状態を変更できないので、何らかの方法で外部に状態を維持しなければなりません。
そうなると MVVM って話になるようなのですが、Model と ViewModel の連携についてはあまりいいサンプルが見つけられなかった。ネットに転がっているサンプルでは UI 操作で ViewModel の変更が完結しているものがほとんどです。このパターンでは Model が View をまたぐ場合のことは触れていないので自分で考えなければいけません。r
結局のところ、モデルをビュー(Activity) の外に置くのが一番簡単な解決策じゃないかと思います。
参考リンクが唯一、Model から ViewModel に更新を通知する例を見つけられたのでそれをそのまま写経してみた。
目次
前提
- Jetpack Compose
- Kotlin
- Android Studio
- Android Studio Koala | 2024.1.1 Patch 1
- Build #AI-241.18034.62.2411.12071903, built on July 11, 2024
基本のビューモデル
まずは、compose を使って ViewModel の LiveData を UI に通知するパターンをつくる。 ついでに ViewModel を Model の一部とみなして Activity の外で宣言する例。ViewModel は別ファイルに分けるべきですが面倒なのでそのままです。
package xxx.yyy.logpanecompose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import red.txn.logpanecompose.ui.theme.LogPaneComposeTheme
class MainActivity : ComponentActivity() {
val vm = MyApp.getInstance().getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// val vm = LogViewModel()
setContent {
LogPaneComposeTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
LogPane(vm=vm, modifier = Modifier.padding(innerPadding))
// LogPane(modifier=Modifier.padding(innerPadding))
}
}
}
}
}
class LogViewModel(): ViewModel() {
val buffer = MutableLiveData("")
private var count: Int = 0
fun append(newLine: String) {
buffer.value = buffer.value + newLine + count.toString() + "\n"
count++
}
}
@Composable
fun LogPane(
modifier: Modifier=Modifier,
vm: LogViewModel = viewModel()
) {
val text: String by vm.buffer.observeAsState("")
Column(modifier=modifier.padding(horizontal=16.dp)) {
Button(
onClick={vm.append("aZZZ")},
modifier=modifier.padding(top=0.dp, bottom=4.dp )
)
{
Text(text="add log")
}
TextField(
value=text,
onValueChange = { },
singleLine = false,
modifier = modifier.fillMaxSize()
)
}
}
@Preview(showBackground = true)
@Composable
fun LogPanePreview() {
LogPaneComposeTheme {
LogPane()
}
}
MainActivity の先頭でアプリケーションクラスから ViewModel を持ってきています。
独自のアプリケーションクラスを書きます
package xxx.yyy.logpanecompose
import android.app.Application
class MyApp: Application() {
public val vm = LogViewModel() // private にするべきかも
companion object {
private var instance: MyApp? = null
fun getInstance(): MyApp {
if (instance == null) {
instance = MyApp()
}
return instance!!
}
}
public fun getViewModel() = vm
}
MyApp を使うように AndroidManifest.xml を変更します。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LogPaneCompose"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.LogPaneCompose">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
LiveData でモデルの変更を受け取る
Model に LiveData を追加して ViewModel で監視するには下のように Model を追加し、 ViewModel を変更する。
// 省略
data class LogModel(val message: String) {
val buffer = MutableLiveData(message)
fun append(newMessage: String) {
buffer.value += newMessage + "\n"
}
}
class LogViewModel(val model: LogModel): ViewModel() {
var buffer = MutableLiveData("")
private val lifecycleOwner = CustomLifecycleOwner()
init {
lifecycleOwner.start()
model.buffer.observe(lifecycleOwner) {
println("viewmodel: $it")
buffer.value = it
}
}
fun append(newMessage: String) {
model.append(newMessage)
}
inner class CustomLifecycleOwner: LifecycleOwner {
private val registry = LifecycleRegistry(this)
override val lifecycle: Lifecycle
get() = registry
fun start() {
registry.currentState = Lifecycle.State.STARTED
}
@Suppress("unused")
fun stop() {
registry.currentState = Lifecycle.State.CREATED
}
}
}
MyApp もモデルを宣言するように変更する
package xxx.yyy.logpanecompose
import android.app.Application
class MyApp: Application() {
val model = LogModel("")
val vm = LogViewModel(model=model)
companion object {
private var instance: MyApp? = null
fun getInstance(): MyApp {
if (instance == null) {
instance = MyApp()
}
return instance!!
}
}
fun getViewModel() = vm
}
StateFlow でモデルの変更を受け取る
Model、ViewModel 層で LiveData を使うのはあまりよくないらしいとのことで、StateFlow パターンもやってみる。
// いろいろ省略
data class LogModel(val message: String) {
// val buffer = MutableLiveData(message)
val buffer = MutableStateFlow(message)
fun append(newMessage: String) {
buffer.value += newMessage + "\n"
}
}
class LogViewModel(val model: LogModel): ViewModel() {
var buffer = MutableLiveData("")
init {
viewModelScope.launch(Dispatchers.IO) {
model.buffer.collect {
println("viewmodel: $it")
buffer.postValue(it)
}
}
}
fun append(newMessage: String) {
model.append(newMessage)
}
}
参考
僕たちはどのようにしてViewModelに通知すべきなのか #Android - Qiita