Dieser Beitrag bietet einen kurzen Ueberblick ueber die notwendigen Schritte, um das MVVM-Muster in einer Kotlin Multiplatform Anwendung anzuwenden.

Der Ausgangspunkt ist eine frisch heruntergeladene Projektvorlage vom Kotlin Multiplatform Wizard.

Dieser Beitrag erklaert nicht, was MVVM ist oder diskutiert seine Vor- und Nachteile. Sein Hauptzweck ist es, als praegnante Notiz ueber die Schritte zu dienen, die ich in einem frischen Projekt unternehme, wenn ich dieses Muster benotige.

Das Ergebnis dieses Beitrags ist eine einfache Anwendung, die einen Zaehler anzeigt. Der Benutzer kann den Zaehler durch Druecken der entsprechenden Buttons starten und stoppen.

So wird es aussehen.

Wir folgen einem Bottom-up-Ansatz und werden diese Teile der App hinzufuegen oder modifizieren:

  • Gradle-Abhaengigkeiten
  • States und Events
  • Repository
  • View-Model
  • View
  • Application

Gradle-Abhaengigkeiten

Fuer meine Multiplatform-Projekte verwende ich MOKO MVVM.

Fuege in gradle/libs.versions.toml die erforderliche Version und Module hinzu.

[versions]
mvvm = "0.16.1"
 
[libraries]
mvvm-compose = { module = "dev.icerock.moko:mvvm-compose", version.ref = "mvvm" }
mvvm-core = { module = "dev.icerock.moko:mvvm-core", version.ref = "mvvm" }

Referenziere dann die Abhaengigkeiten in composeApp/build.gradle.kts.

kotlin {
 androidTarget {
 compilations.all {
  kotlinOptions {
  jvmTarget = "17"
  }
 }
 }
 sourceSets {
 commonMain.dependencies {
  implementation(libs.mvvm.core)
  implementation(libs.mvvm.compose)
 }
 }
}
 
android {
 compileOptions {
 sourceCompatibility = JavaVersion.VERSION_17
 targetCompatibility = JavaVersion.VERSION_17
 }
}

States und Events

Die View und das View Model kommunizieren ausschliesslich ueber States und Events.

Erstelle den State in composeApp/src/commonMain/kotlin/NumberState.kt.

data class NumberState(
 val number: Int
)

Fuege das Event in composeApp/src/commonMain/kotlin/NumberEvent.kt hinzu.

sealed class NumberEvent {
 object Start: NumberEvent()
 object Stop: NumberEvent()
}

Repository

Daten kommen aus dem Repository, erstellt in composeApp/src/commonMain/kotlin/NumberRepository.kt.

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
 
class NumberRepository {
 private val _number = MutableStateFlow(0)
 val number = _number.asStateFlow()
 
 private var job: Job? = null
 
 fun start() {
  job = CoroutineScope(Dispatchers.Default).launch {
   while (true) {
    delay(1000)
    _number.value += 1
   }
  }
 }
 
 fun stop() {
  job?.cancel()
 }
}

View Model

Erstelle das View Model in composeApp/src/commonMain/kotlin/NumberViewModel.kt.

import dev.icerock.moko.mvvm.viewmodel.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
 
class NumberViewModel(val repository: NumberRepository): ViewModel() {
 
 val _state = MutableStateFlow(NumberState(0))
 val state = _state.asStateFlow()
 
 init {
  viewModelScope.launch {
   repository.number.collect { newNumber ->
    _state.update {
     it.copy(number = newNumber)
    }
   }
  }
 }
 
 fun onEvent(event: NumberEvent) {
  when (event) {
   NumberEvent.Start -> repository.start()
   NumberEvent.Stop -> repository.stop()
  }
 }
}

View

Als vorletzten Schritt erstelle die View in composeApp/src/commonMain/kotlin/NumberView.kt.

@Composable
fun NumberView(state: NumberState, onEvent: (NumberEvent) -> Unit) {
 Column(
  modifier = Modifier.fillMaxSize(),
  verticalArrangement = Arrangement.Center,
  horizontalAlignment = Alignment.CenterHorizontally
 ) {
  Column {
   Text(
    text = "${state.number}", fontSize = 64.sp, fontWeight = FontWeight.Bold
   )
  }
 
  Row(
   modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
  ) {
   Button(onClick = { onEvent(NumberEvent.Start) }) {
    Text("Start")
   }
   Button(onClick = { onEvent(NumberEvent.Stop) }) {
    Text("Stop")
   }
  }
 }
}

Application

Schliesslich verbinde alles im Top-Level App Composable: composeApp/src/commonMain/kotlin/App.kt

import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import dev.icerock.moko.mvvm.compose.getViewModel
import dev.icerock.moko.mvvm.compose.viewModelFactory
import org.jetbrains.compose.resources.ExperimentalResourceApi
 
@Composable
fun App() {
 MaterialTheme {
  val numberRepository = NumberRepository()
  val numberViewModel = getViewModel(
   Unit,
   viewModelFactory { NumberViewModel(numberRepository) }
  )
  val state by numberViewModel.state.collectAsState()
 
  NumberView(state, numberViewModel::onEvent)
 }
}

Das war’s - die Standard-Multiplatform-Projektvorlage wurde mit einem MVVM-Muster erweitert, das du nach Bedarf anpassen und erweitern kannst.

Ressourcen