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). confetti_sessions_view.png

Our ViewModels (on both iOS and Android) consume ConfettiRepository from the shared Kotlin Multiplatform code and that in turn exposes a number of StateFlows 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