MobileNativeFoundation / Store

A Kotlin Multiplatform library for building network-resilient applications
https://mobilenativefoundation.github.io/Store/
Apache License 2.0
3.16k stars 197 forks source link

Paging #611

Closed matt-ramotar closed 6 months ago

matt-ramotar commented 6 months ago

[!NOTE]
See #630

Context:

Paging Technical Design Doc

1. Motivations

Powerful and extensible solution for paging in KMP projects, under the Mobile Native Foundation. Address these Android Paging limitations:

2. Overview

Modular and flexible architecture. Using builder, reducer, middleware, and post-reducer effect patterns. Unidirectional data flow. The Pager is the main component. Actions are dispatched through the Pager. The PagerBuilder creates the Pager. It allows configuration of the paging behavior. The PagingSource defines the data loading logic. The PageFetchingStrategy determines when to fetch the next page. The PageAggregatingStrategy combines loaded pages into a single list. The PagingReducer handles state changes based on actions. When an action is dispatched, it goes through the middleware pipeline. The middleware can modify the action. The reducer then updates the state based on the action. After the reducer, we invoke post-reducer effects associated with the action and new state. The updated state is sent back the Pager and emitted to the UI.

3. The Actual Design

3.1 Key Components

3.2 Customizations

Providing many extension points and customization options to tailor behavior. Main customization points:

3.3 Data Flow

Unidirectional data flow. Main steps:

  1. Pager is configured using PagerBuilder and provided an initial key, flow of anchor position, and paging config.
  2. Pager subscribes to the PagingSource to receive paging data updates.
  3. When a PagingAction is dispatched, it goes through the configured PagingMiddleware chain. This enables interception and modification of the action.
  4. The modified action reaches the PagingReducer, which reduces the current PagingState based on the action and returns a new PagingState.
  5. After reduction, any configured PostReducerEffect instances are executed, enabling side effects to be performed based on the new PagingState.
  6. Pager updates PagingStateManager with the new PagingState.
  7. PageFetchingStrategy determines when to fetch the next page of data based on the PagingConfig and current PagingState.
  8. When a new page needs to be fetched, QueueManager enqueues the page key, and the JobCoordinator coordinates the execution of the paging job.
  9. PagingSource loads the requested page and emits the loaded data through the PagingStreamProvider.
  10. The loaded page is stored in the MutablePagingBuffer for efficient retrieval and aggregation.
  11. The PageAggregatingStrategy aggregates the loaded pages into a single list, which is then emitted through the Pager for consumption by the UI.

4. Sample Code

Configuring the Pager using PagerBuilder:

val pager =
    PagerBuilder<Int, CollectionKey, SingleData, CustomAction, CustomError>(
        initialKey = CollectionKey(0),
        anchorPosition = anchorPositionFlow,
        pagingConfig = pagingConfig
    )
        .dispatcher(
            logger = DefaultLogger(),
        ) {

            // Use the default reducer
            defaultReducer {
                errorHandlingStrategy(ErrorHandlingStrategy.PassThrough)
                pagingBufferMaxSize(100)
            }

            middlewares(
                listOf(
                    // Add custom middleware
                )
            )

            // Add custom post reducer effects
            postReducerEffect<MyPagingState, MyPagingAction>(
                state = MyPagingState::class,
                action = MyPagingAction::class,
                effect = MyPostReducerEffect
            )

            // Use the default post reducer effects
            defaultPostReducerEffects(pagingSource = pagingSource)
        }

        .build()

Observing the paging state and dispatching actions:

pager.state.collect { state ->
    when (state) {
        is PagingState.Data.Idle -> {
            // Update UI with loaded data and provide dispatch callback
            DataView(pagingItems = state.data) { action: PagingAction.User ->
                pager.dispatch(action)
            }
        }
        is PagingState.LoadingInitial -> {
            // Show loading indicator
            InitialLoadingView()
        }
        is PagingState.Error -> {
            // Handle error state and provide dispatch callback
            InitialErrorViewCoordinator(errorState = state) { action: PagingAction.User ->
                pager.dispatch(action)
            }
        }
    }
}
matt-ramotar commented 6 months ago

@digitalbuddha @yigit - Putting this back into draft. I'm going to separate this into smaller PRs

matt-ramotar commented 6 months ago

Separated in #630 with https://github.com/MobileNativeFoundation/Store/tree/paging branch