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.


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.

confetti screenshot

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 SavedStateHandlelogic on Android.


Featured in Android Weekly Issue #550