Skip to content

Commit

Permalink
Implement RealDispatcher and Related Classes (#621)
Browse files Browse the repository at this point in the history
* Implement RealDispatcher and Related Classes

Signed-off-by: mramotar <[email protected]>

* Implement RealOptionalInjector

Signed-off-by: mramotar <[email protected]>

* Implement DefaultLogger

Signed-off-by: mramotar <[email protected]>

---------

Signed-off-by: mramotar <[email protected]>
  • Loading branch information
matt-ramotar authored Mar 16, 2024
1 parent 0b901bc commit c38c694
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 0 deletions.
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()
)
}
}

0 comments on commit c38c694

Please sign in to comment.