A frequent question when using KMM (Kotlin Multiplatform Mobile) is firstly whether to share the view model between the iOS and Android clients and, if doing so, how best to do it in a way that still stays true to each platform’s approach to lifecycle, concurrency and observability. I’ve tended in most of my KMM samples to keep the view model in platform level code and also described in last post about how the similar mechanisms available now for managing concurrency on iOS and Android at least make the approaches used very similar. That post was based on the Confetti sample and it’s quite evident I think from this example that there would be value in moving at least part of that view model code in to the shared KMM module. I had been looking at different approaches to how to do this and was very interested to see early alpha release of new KMM-ViewModel library, particularly given that it is based on KMP-NativeCoroutines library which this project was already using.
Initial release of KMM-ViewModel is now available!
— Rick Clephas (@RickClephas) December 17, 2022
Including support for Kotlin 1.8.0-RC which combined with KMP-NativeCoroutines makes sharing your ViewModels effortless 😎https://t.co/rgpNwBJewY
Confetti
Confetti is a KMM sample that currently allows browsing session and speaker information for a range of conferences. It’s based on use of the Apollo Kotlin GraphQL library (the project also includes code for the associated GraphQL backend). Previously it contained individual Swift and Kotlin view models that interacted with a repository in shared code, using data from that then to create immutable UI state consumed by the SwiftUI and Jetpack Compose client code.
Kotlin shared ViewModel
With use now of the KMM-ViewModel library a single shared view model was created as shown below. This is almost identical to the Android AAC view model that the project previously had and is consumed on Android in exactly the same way that one was.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
open class ConfettiViewModel: KMMViewModel(), KoinComponent {
private val repository: ConfettiRepository by inject()
@NativeCoroutinesState
val uiState: StateFlow<SessionsUiState> =
combine(
repository.conferenceName,
repository.sessionsMap,
repository.speakers,
repository.rooms
) { conferenceName, sessionsMap, speakers, rooms, ->
val confDates = sessionsMap.keys.toList().sorted()
val sessionsByStartTimeList = mutableListOf<Map<String, List<SessionDetails>>>()
confDates.forEach { confDate ->
val sessions = sessionsMap[confDate] ?: emptyList()
val sessionsByStartTime = sessions.groupBy { repository.getSessionTime(it) }
sessionsByStartTimeList.add(sessionsByStartTime)
}
SessionsUiState.Success(conferenceName, confDates, sessionsByStartTimeList,
speakers, rooms)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SessionsUiState.Loading)
fun setConference(conference: String) {
repository.setConference(conference)
}
}
sealed interface SessionsUiState {
object Loading : SessionsUiState
data class Success(
val conferenceName: String,
val confDates: List<LocalDate>,
val sessionsByStartTimeList: List<Map<String, List<SessionDetails>>>,
val speakers: List<SpeakerDetails>,
val rooms: List<RoomDetails>
) : SessionsUiState
}
SwiftUI code
The following then is example of how that view model would be consumed in SwiftUI code (with the associated KMM-ViewModel Swift package having been added to the project).
The uiState
StateFlow
in the shared view model is mapped under the hood to a Swift property
such that any changes to the underlying value will cause the appropriate SwiftUI code to be rerendered.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct ConferenceView: View {
@ObservedViewModel var viewModel: ConfettiViewModel
let conference: String
var body: some View {
VStack {
switch viewModel.uiState {
case let uiState as SessionsUiStateSuccess:
TabView {
SessionListView(sessionUiState: uiState)
.tabItem {
Label("Schedule", systemImage: "calendar")
}
SpeakerListView(speakerList: uiState.speakers)
.tabItem {
Label("Speakers", systemImage: "person")
}
}
default:
ProgressView()
}
}
.onAppear {
viewModel.setConference(conference: conference)
}
}
}
“Hybrid” ViewModel support
One other thing worth noting is that it’s also possible to have client specific view models subclass the shared view model thus allowing
a sort of “hybrid” approach where additional platform specific code could be added including for example SavedStateHandle
logic on Android.
Featured in Android Weekly Issue #550 and Kotlin Weekly Issue #343
Related tweet
Wrote (very!) short post about using new KMM-ViewModel (https://t.co/qbrf2i5xbK) library to share view model between the iOS and Android clients in Confetti https://t.co/3Wqa46X31F) sample. Thanks @RickClephas @BoD and @martinbonnin for looking over it!https://t.co/eIo8XRI6E2
— John O'Reilly (@joreilly) December 20, 2022