diff --git a/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/DefaultReducerBuilder.kt b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/DefaultReducerBuilder.kt new file mode 100644 index 000000000..4c39fc167 --- /dev/null +++ b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/DefaultReducerBuilder.kt @@ -0,0 +1,103 @@ +package org.mobilenativefoundation.paging.core + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import org.mobilenativefoundation.paging.core.impl.DefaultAggregatingStrategy +import org.mobilenativefoundation.paging.core.impl.DefaultFetchingStrategy +import org.mobilenativefoundation.paging.core.impl.DefaultReducer +import org.mobilenativefoundation.paging.core.impl.Dispatcher +import org.mobilenativefoundation.paging.core.impl.Injector +import org.mobilenativefoundation.paging.core.impl.JobCoordinator +import org.mobilenativefoundation.paging.core.impl.OptionalInjector +import org.mobilenativefoundation.paging.core.impl.RetriesManager + +/** + * A builder class for creating a default [Reducer] instance. + * + * It enables configuring error handling strategy, aggregating strategy, fetching strategy, custom action reducer, and paging buffer size. + * + * @param Id The type of the unique identifier for each item in the paged data. + * @param K The type of the key used for paging. + * @param P The type of the parameters associated with each page of data. + * @param D The type of the data items. + * @param E The type of errors that can occur during the paging process. + * @param A The type of custom actions that can be dispatched to modify the paging state. + * @param initialKey The initial [PagingKey] used as the starting point for paging. + * @param childScope The [CoroutineScope] in which the reducer will operate. + * @param dispatcherInjector The [Injector] used to provide the [Dispatcher] instance. + * @param loggerInjector The [OptionalInjector] used to provide the optional [Logger] instance. + * @param pagingConfigInjector The [Injector] used to provide the [PagingConfig] instance. + * @param anchorPosition The [StateFlow] representing the anchor position for paging. + */ +class DefaultReducerBuilder, K : Any, P : Any, D : Any, E : Any, A : Any> internal constructor( + private val initialKey: PagingKey, + private val childScope: CoroutineScope, + private val dispatcherInjector: Injector>, + private val loggerInjector: OptionalInjector, + private val pagingConfigInjector: Injector, + private val anchorPosition: StateFlow>, + private val mutablePagingBufferInjector: Injector>, + private val jobCoordinator: JobCoordinator +) { + + private var errorHandlingStrategy: ErrorHandlingStrategy = ErrorHandlingStrategy.RetryLast() + private var aggregatingStrategy: AggregatingStrategy = DefaultAggregatingStrategy() + private var fetchingStrategy: FetchingStrategy = DefaultFetchingStrategy() + private var customActionReducer: UserCustomActionReducer? = null + + /** + * Sets the [ErrorHandlingStrategy] to be used by the reducer. + * + * @param errorHandlingStrategy The [ErrorHandlingStrategy] to be used. + * @return The [DefaultReducerBuilder] instance for chaining. + */ + fun errorHandlingStrategy(errorHandlingStrategy: ErrorHandlingStrategy) = apply { this.errorHandlingStrategy = errorHandlingStrategy } + + /** + * Sets the [AggregatingStrategy] to be used by the reducer. + * + * @param aggregatingStrategy The [AggregatingStrategy] to be used. + * @return The [DefaultReducerBuilder] instance for chaining. + */ + fun aggregatingStrategy(aggregatingStrategy: AggregatingStrategy) = apply { this.aggregatingStrategy = aggregatingStrategy } + + /** + * Sets the [FetchingStrategy] to be used by the reducer. + * + * @param fetchingStrategy The [FetchingStrategy] to be used. + * @return The [DefaultReducerBuilder] instance for chaining. + */ + fun fetchingStrategy(fetchingStrategy: FetchingStrategy) = apply { this.fetchingStrategy = fetchingStrategy } + + /** + * Sets the custom action reducer to be used by the reducer. + * + * @param customActionReducer The [UserCustomActionReducer] to be used. + * @return The [DefaultReducerBuilder] instance for chaining. + */ + fun customActionReducer(customActionReducer: UserCustomActionReducer) = apply { this.customActionReducer = customActionReducer } + + /** + * Builds and returns the configured default [Reducer] instance. + * + * @return The built default [Reducer] instance. + */ + fun build(): Reducer { + val mutablePagingBuffer = mutablePagingBufferInjector.inject() + + return DefaultReducer( + childScope = childScope, + dispatcherInjector = dispatcherInjector, + pagingConfigInjector = pagingConfigInjector, + userCustomActionReducer = customActionReducer, + anchorPosition = anchorPosition, + loggerInjector = loggerInjector, + mutablePagingBuffer = mutablePagingBuffer, + aggregatingStrategy = aggregatingStrategy, + initialKey = initialKey, + retriesManager = RetriesManager(), + errorHandlingStrategy = errorHandlingStrategy, + jobCoordinator = jobCoordinator + ) + } +} diff --git a/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/PagerBuilder.kt b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/PagerBuilder.kt new file mode 100644 index 000000000..94159f464 --- /dev/null +++ b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/PagerBuilder.kt @@ -0,0 +1,311 @@ +package org.mobilenativefoundation.paging.core + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.plus +import org.mobilenativefoundation.paging.core.PagingConfig.InsertionStrategy +import org.mobilenativefoundation.paging.core.impl.DefaultAppLoadEffect +import org.mobilenativefoundation.paging.core.impl.DefaultFetchingStrategy +import org.mobilenativefoundation.paging.core.impl.DefaultLoadNextEffect +import org.mobilenativefoundation.paging.core.impl.DefaultLogger +import org.mobilenativefoundation.paging.core.impl.DefaultPagingSource +import org.mobilenativefoundation.paging.core.impl.DefaultPagingSourceCollector +import org.mobilenativefoundation.paging.core.impl.DefaultUserLoadEffect +import org.mobilenativefoundation.paging.core.impl.DefaultUserLoadMoreEffect +import org.mobilenativefoundation.paging.core.impl.Dispatcher +import org.mobilenativefoundation.paging.core.impl.EffectsHolder +import org.mobilenativefoundation.paging.core.impl.EffectsLauncher +import org.mobilenativefoundation.paging.core.impl.QueueManager +import org.mobilenativefoundation.paging.core.impl.RealDispatcher +import org.mobilenativefoundation.paging.core.impl.RealInjector +import org.mobilenativefoundation.paging.core.impl.RealJobCoordinator +import org.mobilenativefoundation.paging.core.impl.RealMutablePagingBuffer +import org.mobilenativefoundation.paging.core.impl.RealOptionalInjector +import org.mobilenativefoundation.paging.core.impl.RealPager +import org.mobilenativefoundation.paging.core.impl.RealQueueManager +import org.mobilenativefoundation.paging.core.impl.StateManager +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi +import org.mobilenativefoundation.store.store5.MutableStore +import org.mobilenativefoundation.store.store5.Store +import kotlin.reflect.KClass + + +/** + * A builder class for creating a [Pager] instance. + * The [PagerBuilder] enables configuring the paging behavior, + * such as the initial state, initial key, anchor position, middleware, effects, reducer, logger, and paging config. + * + * @param Id The type of the unique identifier for each item in the paged data. + * @param K The type of the key used for paging. + * @param P The type of the parameters associated with each page of data. + * @param D The type of the data items. + * @param E The type of errors that can occur during the paging process. + * @param A The type of custom actions that can be dispatched. + * @param scope The [CoroutineScope] in which the paging operations will be performed. + * @param initialState The initial [PagingState] of the pager. + * @param initialKey The initial [PagingKey] of the pager. + * @param anchorPosition A [StateFlow] representing the anchor position for paging. + */ +class PagerBuilder, K : Any, P : Any, D : Any, E : Any, A : Any>( + scope: CoroutineScope, + initialState: PagingState, + private val initialKey: PagingKey, + private val anchorPosition: StateFlow> +) { + + private val childScope = scope + Job() + private val jobCoordinator = RealJobCoordinator(childScope) + + private var middleware: MutableList> = mutableListOf() + + private var pagingConfigInjector = RealInjector().apply { + instance = PagingConfig(10, 50, InsertionStrategy.APPEND) + } + + private var fetchingStrategyInjector = RealInjector>().apply { + this.instance = DefaultFetchingStrategy() + } + + private var pagingBufferMaxSize = 100 + + private val effectsHolder: EffectsHolder = EffectsHolder() + + private val dispatcherInjector = RealInjector>() + + private val loggerInjector = RealOptionalInjector() + + private val queueManagerInjector = RealInjector>() + private val mutablePagingBufferInjector = RealInjector>().apply { + this.instance = mutablePagingBufferOf(500) + } + + private val insertionStrategyInjector = RealInjector() + private val pagingSourceCollectorInjector = RealInjector>().apply { + this.instance = DefaultPagingSourceCollector() + } + private val pagingSourceInjector = RealInjector>() + + private val stateManager = StateManager(initialState, loggerInjector) + + private var loadNextEffect: LoadNextEffect = DefaultLoadNextEffect(loggerInjector, queueManagerInjector) + + private var appLoadEffect: AppLoadEffect = DefaultAppLoadEffect( + loggerInjector = loggerInjector, + dispatcherInjector = dispatcherInjector, + jobCoordinator = jobCoordinator, + pagingSourceCollectorInjector = pagingSourceCollectorInjector, + pagingSourceInjector = pagingSourceInjector, + stateManager = stateManager + ) + + private var userLoadEffect: UserLoadEffect = DefaultUserLoadEffect( + loggerInjector = loggerInjector, + dispatcherInjector = dispatcherInjector, + jobCoordinator = jobCoordinator, + pagingSourceCollectorInjector = pagingSourceCollectorInjector, + pagingSourceInjector = pagingSourceInjector, + stateManager = stateManager + ) + + private var userLoadMoreEffect: UserLoadMoreEffect = DefaultUserLoadMoreEffect( + loggerInjector = loggerInjector, + dispatcherInjector = dispatcherInjector, + jobCoordinator = jobCoordinator, + pagingSourceCollectorInjector = pagingSourceCollectorInjector, + pagingSourceInjector = pagingSourceInjector, + stateManager = stateManager + ) + + private lateinit var reducer: Reducer + + /** + * Sets the [Reducer] for the pager. + * + * @param reducer The [Reducer] to be used for reducing paging actions and state. + * @return The [PagerBuilder] instance for chaining. + */ + fun reducer(reducer: Reducer) = apply { this.reducer = reducer } + + /** + * Configures the default [Reducer] using the provided [DefaultReducerBuilder]. + * + * @param block A lambda function that takes a [DefaultReducerBuilder] as receiver and allows configuring the default reducer. + * @return The [PagerBuilder] instance for chaining. + */ + fun defaultReducer( + block: DefaultReducerBuilder.() -> Unit + ) = apply { + val builder = DefaultReducerBuilder( + childScope = childScope, + initialKey = initialKey, + dispatcherInjector = dispatcherInjector, + loggerInjector = loggerInjector, + pagingConfigInjector = pagingConfigInjector, + anchorPosition = anchorPosition, + mutablePagingBufferInjector = mutablePagingBufferInjector, + jobCoordinator = jobCoordinator + ) + block(builder) + val reducer = builder.build() + this.reducer = reducer + } + + /** + * Adds an [Effect] to be invoked after reducing the state for the specified [PagingAction] and [PagingState] types. + * + * @param PA The type of the [PagingAction] that triggers the effect. + * @param S The type of the [PagingState] that triggers the effect. + * @param action The [KClass] of the [PagingAction] that triggers the effect. + * @param state The [KClass] of the [PagingState] that triggers the effect. + * @param effect The [Effect] to be invoked. + * @return The [PagerBuilder] instance for chaining. + */ + fun , S : PagingState> effect( + action: KClass>, + state: KClass>, + effect: Effect + ) = apply { + this.effectsHolder.put(action, state, effect) + } + + /** + * Sets the [LoadNextEffect] for the pager. + * + * @param effect The [LoadNextEffect] to be used for loading the next page of data. + * @return The [PagerBuilder] instance for chaining. + */ + fun loadNextEffect(effect: LoadNextEffect) = apply { this.loadNextEffect = effect } + + fun appLoadEffect(effect: AppLoadEffect) = apply { this.appLoadEffect = effect } + fun userLoadEffect(effect: UserLoadEffect) = apply { this.userLoadEffect = effect } + fun userLoadMoreEffect(effect: UserLoadMoreEffect) = apply { this.userLoadMoreEffect = effect } + + /** + * Adds a [Middleware] to the pager. + * + * @param middleware The [Middleware] to be added. + * @return The [PagerBuilder] instance for chaining. + */ + fun middleware(middleware: Middleware) = apply { + this.middleware.add(middleware) + } + + /** + * Sets the [Logger] for the pager. + * + * @param logger The [Logger] to be used for logging. + * @return The [PagerBuilder] instance for chaining. + */ + fun logger(logger: Logger) = apply { this.loggerInjector.instance = logger } + + /** + * Sets the default [Logger] for the pager. + * + * @return The [PagerBuilder] instance for chaining. + */ + fun defaultLogger() = apply { this.loggerInjector.instance = DefaultLogger() } + + /** + * Sets the [PagingConfig] for the pager. + * + * @param pagingConfig The [PagingConfig] to be used for configuring the paging behavior. + * @return The [PagerBuilder] instance for chaining. + */ + fun pagingConfig(pagingConfig: PagingConfig) = apply { this.pagingConfigInjector.instance = pagingConfig } + + /** + * Sets the maximum size of the pager buffer. + * + * @param maxSize The maximum size of the pager buffer. + * @return The [PagerBuilder] instance for chaining. + */ + fun pagerBufferMaxSize(maxSize: Int) = apply { this.mutablePagingBufferInjector.instance = RealMutablePagingBuffer(maxSize) } + + /** + * Sets the [InsertionStrategy] for the pager. + * + * @param insertionStrategy The [InsertionStrategy] to be used for inserting new data into the pager buffer. + * @return The [PagerBuilder] instance for chaining. + */ + fun insertionStrategy(insertionStrategy: InsertionStrategy) = apply { this.insertionStrategyInjector.instance = insertionStrategy } + + fun pagingSourceCollector(pagingSourceCollector: PagingSourceCollector) = apply { this.pagingSourceCollectorInjector.instance = pagingSourceCollector } + + fun pagingSource(pagingSource: PagingSource) = apply { this.pagingSourceInjector.instance = pagingSource } + + fun defaultPagingSource(streamProvider: PagingSourceStreamProvider) = apply { + this.pagingSourceInjector.instance = DefaultPagingSource(streamProvider) + } + + @OptIn(ExperimentalStoreApi::class) + fun mutableStorePagingSource(store: MutableStore, PagingData>, factory: () -> StorePagingSourceKeyFactory) = apply { + this.pagingSourceInjector.instance = DefaultPagingSource( + streamProvider = store.pagingSourceStreamProvider( + keyFactory = factory() + ) + ) + } + + fun storePagingSource(store: Store, PagingData>, factory: () -> StorePagingSourceKeyFactory) = apply { + this.pagingSourceInjector.instance = DefaultPagingSource( + streamProvider = store.pagingSourceStreamProvider( + keyFactory = factory() + ) + ) + } + + private fun provideDefaultEffects() { + this.effectsHolder.put(PagingAction.UpdateData::class, PagingState.Data.Idle::class, this.loadNextEffect) + this.effectsHolder.put(PagingAction.Load::class, PagingState::class, this.appLoadEffect) + this.effectsHolder.put(PagingAction.User.Load::class, PagingState.Loading::class, this.userLoadEffect) + this.effectsHolder.put(PagingAction.User.Load::class, PagingState.Data.LoadingMore::class, this.userLoadMoreEffect) + } + + private fun provideDispatcher() { + val effectsLauncher = EffectsLauncher(effectsHolder) + + val dispatcher = RealDispatcher( + stateManager = stateManager, + middleware = middleware, + reducer = reducer, + effectsLauncher = effectsLauncher, + childScope = childScope + ) + + dispatcherInjector.instance = dispatcher + } + + private fun provideQueueManager() { + val queueManager = RealQueueManager( + pagingConfigInjector = pagingConfigInjector, + loggerInjector = loggerInjector, + dispatcherInjector = dispatcherInjector, + fetchingStrategy = fetchingStrategyInjector.inject(), + pagingBuffer = mutablePagingBufferInjector.inject(), + anchorPosition = anchorPosition, + stateManager = stateManager + ) + + queueManagerInjector.instance = queueManager + } + + /** + * Builds and returns the [Pager] instance. + * + * @return The created [Pager] instance. + */ + fun build(): Pager { + + provideDefaultEffects() + provideDispatcher() + provideQueueManager() + + return RealPager( + initialKey = initialKey, + dispatcher = dispatcherInjector.inject(), + pagingConfigInjector = pagingConfigInjector, + stateManager = stateManager, + ) + } +} \ No newline at end of file diff --git a/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultAggregatingStrategy.kt b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultAggregatingStrategy.kt new file mode 100644 index 000000000..4b0060ef3 --- /dev/null +++ b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultAggregatingStrategy.kt @@ -0,0 +1,35 @@ +package org.mobilenativefoundation.paging.core.impl + +import org.mobilenativefoundation.paging.core.AggregatingStrategy +import org.mobilenativefoundation.paging.core.PagingBuffer +import org.mobilenativefoundation.paging.core.PagingConfig +import org.mobilenativefoundation.paging.core.PagingConfig.InsertionStrategy +import org.mobilenativefoundation.paging.core.PagingData +import org.mobilenativefoundation.paging.core.PagingItems +import org.mobilenativefoundation.paging.core.PagingKey +import org.mobilenativefoundation.paging.core.PagingSource + +class DefaultAggregatingStrategy, K : Any, P : Any, D : Any> : AggregatingStrategy { + override fun aggregate(anchorPosition: PagingKey, prefetchPosition: PagingKey?, pagingConfig: PagingConfig, pagingBuffer: PagingBuffer): PagingItems { + if (pagingBuffer.isEmpty()) return PagingItems(emptyList()) + + val orderedItems = mutableListOf>() + + var currentPage: PagingSource.LoadResult.Data? = pagingBuffer.head() + + while (currentPage != null) { + when (pagingConfig.insertionStrategy) { + InsertionStrategy.APPEND -> orderedItems.addAll(currentPage.collection.items) + InsertionStrategy.PREPEND -> orderedItems.addAll(0, currentPage.collection.items) + InsertionStrategy.REPLACE -> { + orderedItems.clear() + orderedItems.addAll(currentPage.collection.items) + } + } + + currentPage = currentPage.collection.nextKey?.let { pagingBuffer.get(it) } + } + + return PagingItems(orderedItems) + } +} \ No newline at end of file diff --git a/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultFetchingStrategy.kt b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultFetchingStrategy.kt new file mode 100644 index 000000000..bbec5c520 --- /dev/null +++ b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultFetchingStrategy.kt @@ -0,0 +1,26 @@ +package org.mobilenativefoundation.paging.core.impl + +import org.mobilenativefoundation.paging.core.FetchingStrategy +import org.mobilenativefoundation.paging.core.PagingBuffer +import org.mobilenativefoundation.paging.core.PagingConfig +import org.mobilenativefoundation.paging.core.PagingKey +import kotlin.math.max + +class DefaultFetchingStrategy, K : Any, P : Any, D : Any> : FetchingStrategy { + override fun shouldFetch(anchorPosition: PagingKey, prefetchPosition: PagingKey?, pagingConfig: PagingConfig, pagingBuffer: PagingBuffer): Boolean { + if (prefetchPosition == null) return true + + val indexOfAnchor = pagingBuffer.indexOf(anchorPosition) + val indexOfPrefetch = pagingBuffer.indexOf(prefetchPosition) + + if ((indexOfAnchor == -1 && indexOfPrefetch == -1) || indexOfPrefetch == -1) return true + + val effectiveAnchor = max(indexOfAnchor, 0) + val effectivePrefetch = (indexOfPrefetch + 1) * pagingConfig.pageSize + + val shouldFetch = effectivePrefetch - effectiveAnchor < pagingConfig.prefetchDistance + + return shouldFetch + } + +} diff --git a/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultPagingSource.kt b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultPagingSource.kt new file mode 100644 index 000000000..1959e6de5 --- /dev/null +++ b/paging/core/src/commonMain/kotlin/org/mobilenativefoundation/paging/core/impl/DefaultPagingSource.kt @@ -0,0 +1,19 @@ +package org.mobilenativefoundation.paging.core.impl + +import kotlinx.coroutines.flow.Flow +import org.mobilenativefoundation.paging.core.PagingKey +import org.mobilenativefoundation.paging.core.PagingSource +import org.mobilenativefoundation.paging.core.PagingSourceStreamProvider + +class DefaultPagingSource, K : Any, P : Any, D : Any, E : Any, A : Any>( + private val streamProvider: PagingSourceStreamProvider +) : PagingSource { + private val streams = mutableMapOf, Flow>>() + + override fun stream(params: PagingSource.LoadParams): Flow> { + if (params.key !in streams) { + streams[params.key] = streamProvider.provide(params) + } + return streams[params.key]!! + } +} \ No newline at end of file