Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RealDispatcher and Related Classes #621

Merged
merged 3 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.mobilenativefoundation.paging.core

interface Logger {
fun log(message: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.mobilenativefoundation.paging.core.impl

internal object Constants {
const val UNCHECKED_CAST = "UNCHECKED_CAST"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mobilenativefoundation.paging.core.impl

import co.touchlab.kermit.CommonWriter
import org.mobilenativefoundation.paging.core.Logger

class DefaultLogger : Logger {
override fun log(message: String) {
logger.d(
"""
$message
""".trimIndent(),
)
}

private val logger =
co.touchlab.kermit.Logger.apply {
setLogWriters(listOf(CommonWriter()))
setTag("org.mobilenativefoundation.paging")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.PagingAction

/**
* A dispatcher for handling paging actions and dispatching them to the appropriate middleware and reducer.
*
* @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.
*/
interface Dispatcher<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any> {

/**
* Dispatches a paging action to the middleware and reducer chain.
*
* @param PA The type of the paging action being dispatched.
* @param action The paging action to dispatch.
* @param index The index of the middleware to start dispatching from. Default is 0.
*/
fun <PA : PagingAction<Id, K, P, D, E, A>> dispatch(action: PA, index: Int = 0)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.Effect
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingState
import org.mobilenativefoundation.paging.core.impl.Constants.UNCHECKED_CAST
import kotlin.reflect.KClass

/**
* A type alias representing a mapping from a [PagingAction] to a list of [Effect]s.
*/
typealias PagingActionToEffects<Id, K, P, D, E, A> = MutableMap<KClass<out PagingAction<Id, K, P, D, E, A>>, MutableList<Effect<Id, K, P, D, E, A, *, *>>>

/**
* A type alias representing a mapping from a [PagingState] to a [PagingActionToEffects] map.
*/
typealias PagingStateToPagingActionToEffects<Id, K, P, D, E, A> = MutableMap<KClass<out PagingState<Id, K, P, D, E>>, PagingActionToEffects<Id, K, P, D, E, A>>

/**
* A class for holding and managing effects based on [PagingAction] and [PagingState] types.
*
* @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.
*/
@Suppress(UNCHECKED_CAST)
class EffectsHolder<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any> {
private val effects: PagingStateToPagingActionToEffects<Id, K, P, D, E, A> = mutableMapOf()

/**
* Retrieves the list of effects associated with the specified [PagingAction] and [PagingState] types.
*
* @param PA The type of the [PagingAction].
* @param S The type of the [PagingState].
* @param action The [KClass] of the [PagingAction].
* @param state The [KClass] of the [PagingState].
* @return The list of effects associated with the specified [PagingAction] and [PagingState] types.
*/
fun <PA : PagingAction<Id, K, P, D, E, A>, S : PagingState<Id, K, P, D, E>> get(
action: KClass<out PagingAction<Id, K, P, D, E, A>>,
state: KClass<out PagingState<Id, K, P, D, E>>
): List<Effect<Id, K, P, D, E, A, PA, S>> {
action as KClass<PA>
state as KClass<S>

return effects[state]?.get(action) as? List<Effect<Id, K, P, D, E, A, PA, S>> ?: emptyList()
}

/**
* Adds an effect to the list of effects associated with the specified [PagingAction] and [PagingState] types.
*
* @param PA The type of the [PagingAction].
* @param S The type of the [PagingState].
* @param action The [KClass] of the [PagingAction].
* @param state The [KClass] of the [PagingState].
* @param effect The effect to add.
*/
fun <PA : PagingAction<Id, K, P, D, E, A>, S : PagingState<Id, K, P, D, E>> put(
action: KClass<out PagingAction<*, *, *, *, *, *>>,
state: KClass<out PagingState<*, *, *, *, *>>,
effect: Effect<Id, K, P, D, E, A, PA, S>
) {
action as KClass<out PA>
state as KClass<out S>

if (state !in effects) {
effects[state] = mutableMapOf()
}

if (action !in effects[state]!!) {
effects[state]!![action] = mutableListOf()
}

effects[state]!![action]!!.add(effect)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingState
import org.mobilenativefoundation.paging.core.impl.Constants.UNCHECKED_CAST
import kotlin.reflect.KClass

/**
* A class for launching effects based on dispatched [PagingAction]s and the current [PagingState].
*
* @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.
* @property effectsHolder The [EffectsHolder] instance holding the effects.
*/
@Suppress(UNCHECKED_CAST)
class EffectsLauncher<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any>(
private val effectsHolder: EffectsHolder<Id, K, P, D, E, A>
) {

fun <PA : PagingAction<Id, K, P, D, E, A>, S : PagingState<Id, K, P, D, E>> launch(action: PA, state: S, dispatch: (PagingAction<Id, K, P, D, E, A>) -> Unit) {

effectsHolder.get<PA, S>(action::class, state::class).forEach { effect ->
effect(action, state, dispatch)
}

effectsHolder.get<PA, PagingState<Id, K, P, D, E>>(action::class, PagingState::class as KClass<out PagingState<Id, K, P, D, E>>).forEach { effect ->
effect(action, state, dispatch)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mobilenativefoundation.paging.core.impl

/**
* An interface representing an injector for providing instances of a specific type.
*
* @param T The type of the instance to be injected.
*/
interface Injector<T : Any> {

/**
* Injects an instance of type [T].
*
* @return The injected instance.
*/
fun inject(): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.mobilenativefoundation.paging.core.impl

/**
* An interface representing an optional injector for providing instances of a specific type.
*
* @param T The type of the instance to be injected.
*/
interface OptionalInjector<T : Any> {
/**
* Injects an instance of type [T] if available, or returns null.
*
* @return The injected instance or null if not available.
*/
fun inject(): T?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.mobilenativefoundation.paging.core.impl

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.mobilenativefoundation.paging.core.Middleware
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingState
import org.mobilenativefoundation.paging.core.Reducer

/**
* A real implementation of the [Dispatcher] interface for handling paging actions and managing the paging state.
*
* @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.
* @property stateManager The [StateManager] instance for managing the paging state.
* @property middleware The list of [Middleware] instances to be applied to the dispatched actions.
* @property reducer The [Reducer] instance for reducing the paging state based on the dispatched actions.
* @property effectsLauncher The [EffectsLauncher] instance for launching effects based on the dispatched actions and the current state.
* @property childScope The [CoroutineScope] in which the dispatcher will operate.
*/
class RealDispatcher<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any>(
private val stateManager: StateManager<Id, K, P, D, E>,
private val middleware: List<Middleware<Id, K, P, D, E, A>>,
private val reducer: Reducer<Id, K, P, D, E, A>,
private val effectsLauncher: EffectsLauncher<Id, K, P, D, E, A>,
private val childScope: CoroutineScope,
) : Dispatcher<Id, K, P, D, E, A> {

/**
* Dispatches a paging action to the middleware and reducer chain.
*
* @param PA The type of the paging action being dispatched.
* @param action The paging action to dispatch.
* @param index The index of the middleware to start dispatching from.
*/
override fun <PA : PagingAction<Id, K, P, D, E, A>> dispatch(action: PA, index: Int) {
if (index < middleware.size) {

childScope.launch {
middleware[index].apply(action) { nextAction ->
dispatch(nextAction, index + 1)
}
}

} else {
childScope.launch {
reduceAndLaunchEffects(action)
}
}
}

/**
* Reduces the paging state based on the dispatched action and launches the corresponding effects.
*
* @param PA The type of the paging action being dispatched.
* @param action The paging action to reduce and launch effects for.
*/
private suspend fun <PA : PagingAction<Id, K, P, D, E, A>> reduceAndLaunchEffects(action: PA) {
val prevState = stateManager.state.value
val nextState = reducer.reduce(action, prevState)

stateManager.update(nextState)

when (nextState) {
is PagingState.Initial -> effectsLauncher.launch(action, nextState, ::dispatch)
is PagingState.Data.Idle -> effectsLauncher.launch(action, nextState, ::dispatch)
is PagingState.Data.ErrorLoadingMore<Id, K, P, D, E, *> -> effectsLauncher.launch(action, nextState, ::dispatch)
is PagingState.Data.LoadingMore -> effectsLauncher.launch(action, nextState, ::dispatch)
is PagingState.Error.Custom -> effectsLauncher.launch(action, nextState, ::dispatch)
is PagingState.Error.Exception -> effectsLauncher.launch(action, nextState, ::dispatch)
is PagingState.Loading -> effectsLauncher.launch(action, nextState, ::dispatch)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.mobilenativefoundation.paging.core.impl

internal class RealInjector<T : Any> : Injector<T> {
var instance: T? = null

override fun inject(): T {
return instance!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.mobilenativefoundation.paging.core.impl

class RealOptionalInjector<T : Any> : OptionalInjector<T> {
var instance: T? = null

override fun inject(): T? {
return instance
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.mobilenativefoundation.paging.core.impl

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.mobilenativefoundation.paging.core.Logger
import org.mobilenativefoundation.paging.core.PagingState

/**
* A class for managing the state of the paging process.
*
* @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 initialState The initial [PagingState].
* @param loggerInjector The [OptionalInjector] for providing a [Logger] instance.
*/
class StateManager<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any>(
initialState: PagingState<Id, K, P, D, E>,
loggerInjector: OptionalInjector<Logger>
) {

private val logger = lazy { loggerInjector.inject() }

private val _state = MutableStateFlow(initialState)
val state = _state.asStateFlow()

/**
* Updates the state with the specified [PagingState].
*
* @param nextState The next [PagingState] to update the state with.
*/
fun update(nextState: PagingState<Id, K, P, D, E>) {

log(nextState)

_state.value = nextState
}

private fun log(nextState: PagingState<Id, K, P, D, E>) {
logger.value?.log(
"""
Updating state:
Previous state: ${_state.value}
Next state: $nextState
""".trimIndent()
)
}
}
Loading