This week saw the first Kotlin Multiplatform (KMP) stable release of the Jetpack Paging library (following on from a flurry of Jetpack KMP related announcements recently!). In this article I’m going to outline changes made to add use of that library to the Morty KMP sample and in particular show how that could be consumed in the associated SwiftUI client. We’ll also cover the Android Compose code for completeness but that works as it did before when using the previously Android only version of that library.
Wow, lots of AI and Gemini at #GoogleIO today, but we couldn't pass up the opportunity to also include some #JetpackReleaseNotes with Lifecycle 2.8.0 and Paging 3.3.0 having their first KMP stable releases! Plus, ViewPager2 1.1.0 and Fragment 1.7.1!https://t.co/vyCuwEI9vC
— Ian Lake (@ianhlake) May 15, 2024
Project overview
Morty is a Kotlin Multiplatform sample that demonstrates use of GraphQL in shared KMP code using the Apollo Kotlin library and includes Jetpack Compose and SwiftUI clients (based on https://github.com/Dimillian/MortyUI SwiftUI project). The following are screenshots of the Android and iOS clients.
Updates to shared KMP code
The initial changes involved just moving the Jetpack Paging common dependency and existing PagingSource
classes from the
Android module to the shared KMP one. For example CharactersDataSource
shown below which uses the repository class to fetch data for
a particular page from the GraphQL backend (the repository makes use in turn of the Apollo Kotlin library).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CharactersDataSource(private val repository: MortyRepository) : PagingSource<Int, CharacterDetail>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CharacterDetail> {
val pageNumber = params.key ?: 0
val charactersResponse = repository.getCharacters(pageNumber)
val characters = charactersResponse.results.mapNotNull { it?.characterDetail }
val prevKey = if (pageNumber > 0) pageNumber - 1 else null
val nextKey = charactersResponse.info.next
return LoadResult.Page(data = characters, prevKey = prevKey, nextKey = nextKey)
}
override fun getRefreshKey(state: PagingState<Int, CharacterDetail>): Int? {
return null
}
}
That Android module had also included view models where Pager
instances had been created.
That code, along with those View Models, were also moved to shared code (making use of
the KMP-ObservableViewModel library
to share the view models across the platforms)
1
2
3
4
5
6
7
8
9
open class CharactersViewModel(): ViewModel(), KoinComponent {
private val repository: MortyRepository by inject()
val charactersFlow: Flow<PagingData<CharacterDetail>> = Pager(PagingConfig(pageSize = 20)) {
CharactersDataSource(repository)
}.flow.cachedIn(viewModelScope.coroutineScope)
...
}
The open question then was what was needed in the shared code to expose that data to the iOS SwiftUI client and thankfully received following suggestion from Ian Lake!
I’d be interested if you’re able to hook up the (newly public) PagingDataPresenter to something like UICollectionViewDiffableDataSource to get Paging hooked up to a SwiftUI based UI. In theory, you should have everything you need to get that working!
These are the changes added then to the view model to make us of PagingDataPresenter
. This code hooks up that class to the
Pager
one shown above. Note use of that getElement
function below. We need to call this from the SwiftUI client as per
following comnent from Ian to replicate functionality in
LazingPagingItems that also calls charactersPagingDataPresenter.get()
.
Yes, that’s how it is supposed to work (and the big difference between peek() and get()) - get() is what sends the (internal) view port hints that is what triggers the automatic loading as you scroll down/up beyond the data already loaded.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private val charactersPagingDataPresenter = object : PagingDataPresenter<CharacterDetail>() {
override suspend fun presentPagingDataEvent(event: PagingDataEvent<CharacterDetail>) {
updateCharactersSnapshotList()
}
}
@NativeCoroutinesState
val charactersSnapshotList = MutableStateFlow<ItemSnapshotList<CharacterDetail>>(viewModelScope, charactersPagingDataPresenter.snapshot())
init {
viewModelScope.coroutineScope.launch {
charactersFlow.collectLatest {
charactersPagingDataPresenter.collectFrom(it)
}
}
}
private fun updateCharactersSnapshotList() {
charactersSnapshotList.value = charactersPagingDataPresenter.snapshot()
}
fun getElement(index: Int): CharacterDetail? {
return charactersPagingDataPresenter.get(index)
}
iOS SwiftUI client code
The following is the SwiftUI code to show the list of characters. It makes use of CharactersViewModel
from the shared
KMP code and also functionality provided by the KMP-ObservableViewModel
library to allow a MutableStateFlow
from the shared code to appear as a standard observable Swift property (in this case for charactersSnapshotList
).
Note the call to getElement
as mentioned above. I’m not certain there isn’t a better way to do this and will update the
article if I receive any feedback about that.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct CharactersListView: View {
@StateViewModel var viewModel = CharactersViewModel()
var body: some View {
List {
ForEach(viewModel.charactersSnapshotList.indices, id: \.self) { index in
if let character = viewModel.getElement(index: Int32(index)) {
CharactersListRowView(character: character)
}
}
}
.navigationTitle("Characters")
}
}
Android Compose client code
The Android Compose code is more or less the same as it was before. The only difference is use of that same CharactersViewModel
used by the
iOS SwiftUI client.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
fun CharactersListView(characterSelected: (character: CharacterDetail) -> Unit) {
val viewModel: CharactersViewModel = koinInject()
val lazyCharacterList = viewModel.charactersFlow.collectAsLazyPagingItems()
LazyColumn {
items(
count = lazyCharacterList.itemCount,
key = lazyCharacterList.itemKey { it.id }
) { index ->
val character = lazyCharacterList[index]
character?.let {
CharactersListRowView(character, characterSelected)
}
}
}
}
Featured in Android Weekly #623
Related tweet
Consuming Jetpack Paging KMP code in SwiftUI and Compose clients https://t.co/iqAu2ukKcN
— John O'Reilly (@joreilly) May 17, 2024
As mentioned in article, not 100% certain about SwiftUI implementation here but will update if/when I find better way (and thanks again to @ianhlake for pointers)