In the Declarative UI world we live in today a ViewModel typically acts (among other things) as a container of observable UI state, changes to which will cause the appropriate parts of the UI to be re-rendered. This UI state is frequently derived from different pieces of information coming from our domain or data layer (and potentially from actions a user can take). In this article I’m going to compare parts of the iOS and Android ViewModels for the Confetti KMM GraphQL sample and show how the relatively recent alignment of approaches to structured concurrency in Swift and Kotlin has allowed us to create quite similar implementations on both platforms (including the seamless consumption of Kotlin Flows exposed from a repository in shared KMM code).
The particular example we’re going to look at here is based on the following screen where we show the list of conference sessions for a particular date (with user able
to change that date in a SwiftUI Picker
/Compose TabRow
at the top of screen).
Our ViewModels (on both iOS and Android) consume ConfettiRepository
from the shared Kotlin Multiplatform code and that in turn exposes a number of StateFlow
s
containing the conference information we’re interested in (based on results of queries to a GraphQL backend using the
Apollo library).
If we look at the ViewModels themselves we can hopefully see how similar the approaches are now to creating the observable UI state based on that information from the repository.
- On Android we’re consuming the Kotlin Flows directly and creating that UI state then using call to combine.
- On iOS those Flows are mapped to Swift AsyncSequences (using KMP-NativeCoroutines library) and the UI state then is created in a very similar manner using combineLatest from the Swift Async Extensions library.
- In both cases the date the user has selected is inclued in those combine* calls, allowing the creation of single, immutable source of truth for what we need to show in the UI.
iOS Swift ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Published public var selectedDateIndex: Int = 0
@Published public var uiState: SessionsUiState = .loading
...
Task {
let confDatesAsyncSequence = asyncStream(for: repository.confDatesNative)
let sessionsMapAsyncSequence = asyncStream(for: repository.sessionsMapNative)
for try await (confDates, sessionsMap, selectedDateIndex)
in combineLatest(confDatesAsyncSequence, sessionsMapAsyncSequence, $selectedDateIndex.values) {
let selectedDate = confDates[selectedDateIndex]
let sessions = sessionsMap[selectedDate] ?? []
self.uiState = SessionsUiState.success(confDates, selectedDateIndex, sessions)
}
}
Android Kotlin ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var selectedDateIndex = MutableStateFlow<Int>(0)
val uiState: StateFlow<SessionsUiState> =
combine(
repository.confDates,
repository.sessionsMap,
selectedDateIndex
) { confDates, sessionsMap, selectedDateIndex ->
val selectedDate = confDates[selectedDateIndex]
val sessions = sessionsMap[selectedDate] ?: emptyList()
SessionsUiState.Success(confDates, selectedDateIndex, sessions)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SessionsUiState.Loading)
When the user switches dates in the UI that will cause selectedDateIndex
to be updated in the ViewModel, which in turn will trigger combine
/combineLatest
to pick up that change and re-generate the UI state. Note on iOS we’re using Swift Async Extensions .values
to convert selectedDateIndex
to an AsyncSequence
.
Featured in Android Weekly Issue #538 and Kotlin Weekly Issue #322
Related tweet
Wrote very short article showing example of how aligned the implementations of iOS and Android ViewModels can be when consuming Flows from shared Kotlin Multiplatform code https://t.co/U8wVfQ13lX. #KMM #iOSDev #AndroidDev
— John O'Reilly (@joreilly) October 1, 2022