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.


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.

Screenshots


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)
            }
        }
    }
}