fr.haan.bipak:bipak-core-js

Core library of BiPaK is a Kotlin multiplatform paging library.


License
Wsuipa

Documentation

BiPaK logo

What is BiPaK?

BiPaK is a Kotlin multiplatform paging library. It is inspired by Jetpack Paging 3 but tries to be less a black box regarding data flows.

Target architecture

The library is designed to fit into a clean architecture implementation, support common UI patterns (MVP, MVVM, MVI...) and to be used with Kotlin Multiplatform.

Résultat logo

Usage

Create the DataSource

The first step is to implement a PagingDataSource by sub-classing it. It has 2 types parameters: Key and Value Key is the type used to identify a page (often an Int for a page index) and the value is the type of the data in the list.

The load() method has to do what's needed to fetch the data from a network source or a database. it can return a page content by returning a PagingDataSource.LoadResult.Page. In case of error, it can be exposed using PagingDataSource.LoadResult.Error

try {
    // Get data from data source
    val data = dataSource.getData(params.key ?: 0, params.loadSize)
    val retData = data.items.map { it.toDomain() }

    // Return data and metadata
    return LoadResult.Page(
        data = retData,
        prevKey = null,
        nextKey = data.metadata.nextPage,
        totalCount = data.metadata.totalCount,
    )
} catch (error: Throwable) {
    // Report the error if any
    return LoadResult.Error(error)
}

Instantiate a Pager

The Pager will take the PagingDataSource previously defined and some configuration:

private val pager = Pager(
    scope = coroutineScope,
    source = dataSoure,
    initialKey = 0,
    // Optional config parameter:
    // config = PagingConfig(pageSize = 10, prefetchDistance = 5)
)

The Pager can the expose a Flow<PagingData> using:

val flow : Flow<PagingData<Value>> = pager.dataFlow

This flow contains the state and all the fetched data and can be consumed by the View or any other class that needs it.

Android RecyclerView

BiPaK Sample for RecyclerView screenshot

For RecyclerView, PagingDataAdapter can be used.

It can be retrieved using this dependency:

dependencies {
    implementation("fr.haan.bipak:bipak-android:0.9.0")
}

It is based on ListAdapter and therefore needs a DiffUtil.ItemCallback implementation for the list elements type.

PagingDataAdapter has to be implemented and linked to a RecyclerView like any ListAdapter:

val adapter = UiModelAdapter()
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = adapter

We then need to setup the communication between the PagingDataAdapter and the Pager.

lifecycleScope.launchWhenResumed {
    // pass the flow from the adapter to the pager
    pager.subscribeToViewEvents(adapter.eventFlow)
}

lifecycleScope.launchWhenResumed {
    // Get the flow from the pager and pass it to the adapter
    pager.dataFlow.collectLatest {
        // We assume that the pager exposes Domain objects
        // that needs to be converted using .toUiModel()
        adapter.submitList(it.toUiModel())
    }
}

When screen is ready to be displayed and data has to be fetched, just call:

adapter.start()

The data should be fetched as the list is scrolled.

Jetpack Compose

BiPaK Sample for Jetpack Compose screenshot

Helpers are also provided for Android Jetpack Compose It can be retrieved using this dependency:

dependencies {
    implementation("fr.haan.bipak:bipak-compose-android:0.9.0")
}

The function collectAsLazyPagingItems() is provided to expose a LazyPagingItems instance.

val pagingData: LazyPagingItems<T> = pager.dataFlow.collectAsLazyPagingItems()

We then need to setup the communication between the LazyPagingItems and the Pager.

launch {
    pager.subscribeToViewEvents(pagingData.eventFlow)
}

We can then handle received elements using a Compose LazyColumn. Two helpers are provided: items() and itemsIndexed():

        LazyColumn
        {
            itemsIndexed(pagingDataState = pagingData, itemContent = { index, item ->
            // Map item to a ListItem composable
                item?.let { ListItem(index, it.content) }
            })

            // Handle Load & Error states by adding a last item to the list
            when (val loadState = pagingData.loadState) {
                PagingData.LoadState.Loading -> {
                    item { LoadingListItem() }
                }

                is PagingData.LoadState.Error -> {
                    item { ErrorListItem(loadState.error.message.orEmpty()) { pagingData.retry() } }
                }

                PagingData.LoadState.NotLoading -> { /* noop */
                }
            }
        }

Swift Usage

Flow<PagingData> can be handled directly in Swift. KMP-NativeCoroutines is used to consume it in the provided samples.

To provide scrolling event to the Pager, we have to instantiate a PagingEventEmitter. The flow of paging events has to be passed to the Pager instance. For instance, we are using a Repository defined in the common multiplatform module to achieve this.

repository.setViewEventFlow(eventFlow: eventEmitter.eventFlow)

We also have to request the first item to trigger the fetching of the first page:

eventEmitter.onGetItem(index: 0)

UIKit

BiPaK Sample for UIKit screenshot

To display the list, we are using a UITableView backed by UITableViewDiffableDataSource data source.

eventEmitter.onGetItem() has to be called in the cellProvider closure of UITableViewDiffableDataSource.

 cellProvider: { tableView, indexPath, data in
// ...
    case .Data(let item):
        let cell = tableView.dequeueReusableCell(
            withIdentifier: ItemTableViewCell.identifier,
            for: indexPath)
        // Calling event emitter with current index
        self.eventEmitter.onGetItem(index: Int32(indexPath.row))
        cell.textLabel?.text = item.content
        return cell
// ...

You may take a look at the sample provided for a complete integration example.

SwiftUI

BiPaK Sample for SwiftUI screenshot

To consume the Flow<PagingData> with SwiftUI we are setting up an ObservableObject to connect with the common Kotlin Multiplatform repository.

To trigger scrolling events, PagingView has to be used to encapsulate the item View.

var body: some View {
    ScrollView {
        LazyVStack {
            ForEach(
                Array(viewModel.pagingData.list.enumerated()),
                id: \.offset
            ) { index, element in
                PagingView(eventEmitter: viewModel.eventEmitter, id: index) {
                    Text(element.content)
                }
            }
            if(viewModel.pagingData.state is PagingDataLoadState.Loading) {
                ProgressView()
            }
            if let state = viewModel.pagingData.state as? PagingDataLoadState.Error {
                HStack {
                    Text(state.error.message ?? "Unknown error")
                    Button("Retry") {
                        viewModel.eventEmitter.retry()
                    }
                }
            }
            
        }
    }
}

You may take a look at the sample provided for a complete integration example.

What does BiPaK mean?

The name comes from contractions and puns based on this: Bibliothèque de Pagination et de Cache, french for Paging and Cache Library. First two letters of each word gives: BiPaCa And as Ca is pronounced like the letter K in french and as lot of Kotlin libraries contains a K, BiPaK was chosen.

API Reference

See API documentation

Made with ❤️ at BAM.tech