Skip to content

Commit

Permalink
refactor mvi arch
Browse files Browse the repository at this point in the history
  • Loading branch information
stslex committed Dec 9, 2024
1 parent 94eee15 commit bd4f3d2
Show file tree
Hide file tree
Showing 23 changed files with 507 additions and 219 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

abstract class BaseStore<S : State, A : Action, E : Event>(initialState: S) : ViewModel(),
Store<S, A, E> {
abstract class BaseStore<S : State, A : Action, E : Event>(
initialState: S
) : ViewModel(), Store<S, A, E> {

private val _event: MutableSharedFlow<E> = MutableSharedFlow()
override val event: SharedFlow<E> = _event.asSharedFlow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ interface Store<out S : State, in A : Action, out E : Event> {

interface State

interface Event
interface Event {

data class Handler(val action: Action) : Event
}

interface Action {

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.stslex.wizard.core.ui.mvi.store_di

import com.stslex.wizard.core.ui.mvi.v2.BaseStore
import org.koin.core.definition.BeanDefinition
import org.koin.core.definition.KoinDefinition
import org.koin.core.module.Module

inline fun <reified R : BaseStore<*, *, *>> Module.storeOf(
crossinline constructor: () -> R,
noinline options: (BeanDefinition<R>.() -> Unit)? = null,
): KoinDefinition<R> = viewModelOf(constructor, options)

inline fun <reified R : BaseStore<*, *, *>, reified T1> Module.storeOf(
crossinline constructor: (T1) -> R,
noinline options: (BeanDefinition<R>.() -> Unit)? = null,
): KoinDefinition<R> = viewModelOf(constructor, options)

inline fun <reified R : BaseStore<*, *, *>, reified T1, reified T2> Module.storeOf(
crossinline constructor: (T1, T2) -> R,
noinline options: (BeanDefinition<R>.() -> Unit)? = null,
): KoinDefinition<R> = viewModelOf(constructor, options)

inline fun <reified R : BaseStore<*, *, *>, reified T1, reified T2, reified T3> Module.storeOf(
crossinline constructor: (T1, T2, T3) -> R,
noinline options: (BeanDefinition<R>.() -> Unit)? = null,
): KoinDefinition<R> = viewModelOf(constructor, options)

inline fun <reified R : BaseStore<*, *, *>, reified T1, reified T2, reified T3, reified T4> Module.storeOf(
crossinline constructor: (T1, T2, T3, T4) -> R,
noinline options: (BeanDefinition<R>.() -> Unit)? = null,
): KoinDefinition<R> = viewModelOf(constructor, options)

inline fun <reified R : BaseStore<*, *, *>, reified T1, reified T2, reified T3, reified T4, reified T5> Module.storeOf(
crossinline constructor: (T1, T2, T3, T4, T5) -> R,
noinline options: (BeanDefinition<R>.() -> Unit)? = null,
): KoinDefinition<R> = viewModelOf(constructor, options)
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.stslex.wizard.core.ui.mvi.v2

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.stslex.wizard.core.core.AppDispatcher
import com.stslex.wizard.core.core.coroutine.AppCoroutineScope
import com.stslex.wizard.core.core.coroutine.AppCoroutineScopeImpl
import com.stslex.wizard.core.ui.mvi.Store
import com.stslex.wizard.core.ui.mvi.Store.Action
import com.stslex.wizard.core.ui.mvi.Store.Event
import com.stslex.wizard.core.ui.mvi.Store.State
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

open class BaseStore<S : State, A : Action, E : Event>(
initialState: S,
private val handlers: Set<Handler<S, *, E, A>>
) : ViewModel(), Store<S, A, E>, HandlerStore<S, A, E> {

private val _event: MutableSharedFlow<E> = MutableSharedFlow()
override val event: SharedFlow<E> = _event.asSharedFlow()

private val _state: MutableStateFlow<S> = MutableStateFlow(initialState)
override val state: StateFlow<S> = _state.asStateFlow()

protected val scope: AppCoroutineScope = AppCoroutineScopeImpl(viewModelScope)

private var _lastAction: A? = null
override val lastAction: A?
get() = _lastAction

@Suppress("UNCHECKED_CAST")
override fun sendAction(action: A) {
if (lastAction != action && action !is Action.RepeatLast) {
_lastAction = action
}
val handler = handlers.firstOrNull { it.checkAction(action) } as? Handler<S, A, E, A>
?: throw IllegalStateException("Handler not found for action: ${action::class.simpleName}")
handler.invoke(this, action)
}

/**
* Updates the state of the screen.
* @param update - function that updates the state
* */
override fun updateState(update: (S) -> S) {
_state.update(update)
}

/**
* Sends an event to the screen. The event is sent on the default dispatcher of the AppDispatcher.
* @param event - event to be sent
* @see AppDispatcher
* */
override fun sendEvent(event: E) {
viewModelScope.launch { _event.emit(event) }
}

/**
* Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher.
* @param onError - error handler
* @param onSuccess - success handler
* @param action - action to be executed
* @return Job
* @see Job
* @see AppDispatcher
* */
override fun <T> launch(
onError: suspend (Throwable) -> Unit,
onSuccess: suspend CoroutineScope.(T) -> Unit,
workDispatcher: CoroutineDispatcher,
eachDispatcher: CoroutineDispatcher,
action: suspend CoroutineScope.() -> T,
) = scope.launch(
onError = onError,
workDispatcher = workDispatcher,
eachDispatcher = eachDispatcher,
onSuccess = onSuccess,
action = action
)

/**
* Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher.
* @param onError - error handler
* @param each - action for each element of the flow
* @return Job
* @see Flow
* @see Job
* @see AppDispatcher
* */
override fun <T> Flow<T>.launch(
onError: suspend (cause: Throwable) -> Unit,
workDispatcher: CoroutineDispatcher,
eachDispatcher: CoroutineDispatcher,
each: suspend (T) -> Unit
): Job = scope.launch(
flow = this,
workDispatcher = workDispatcher,
eachDispatcher = eachDispatcher,
onError = onError,
each = each,
)


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stslex.wizard.core.ui.mvi.v2

import com.stslex.wizard.core.ui.mvi.Store.Action
import com.stslex.wizard.core.ui.mvi.Store.Event
import com.stslex.wizard.core.ui.mvi.Store.State

internal class BaseStoreImpl<S : State, A : Action, E : Event>(
initialState: S,
handlers: Set<Handler<S, *, E, A>>
) : BaseStore<S, A, E>(
initialState = initialState,
handlers = handlers
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.stslex.wizard.core.ui.mvi.v2

import com.stslex.wizard.core.ui.mvi.Store.Action
import com.stslex.wizard.core.ui.mvi.Store.Event
import com.stslex.wizard.core.ui.mvi.Store.State
import kotlin.reflect.KClass

abstract class Handler<S : State, A : StoreAction, E : Event, StoreAction : Action>(val actionKClass: KClass<*>) {

val handlerName: String = requireNotNull(actionKClass.simpleName) {
"Action class name is null"
}

inline fun checkAction(action: StoreAction): Boolean = actionKClass.isInstance(action)

abstract fun HandlerStore<S, StoreAction, E>.invoke(action: A)

override fun toString(): String = handlerName

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Handler<*, *, *, *>) return false
return handlerName == other.handlerName
}

override fun hashCode(): Int = handlerName.hashCode()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.stslex.wizard.core.ui.mvi.v2

import com.stslex.wizard.core.core.AppDispatcher
import com.stslex.wizard.core.core.AppDispatcherImpl
import com.stslex.wizard.core.ui.mvi.Store
import com.stslex.wizard.core.ui.mvi.Store.Event
import com.stslex.wizard.core.ui.mvi.Store.State
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

interface HandlerStore<S : State, A : Store.Action, E : Event> {

val state: StateFlow<S>

val lastAction: A?

fun sendEvent(event: E)

fun sendAction(action: A)

fun updateState(update: (S) -> S)

/**
* Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher.
* @param onError - error handler
* @param onSuccess - success handler
* @param action - action to be executed
* @return Job
* @see Job
* @see AppDispatcher
* */
fun <T> launch(
onError: suspend (Throwable) -> Unit = {},
onSuccess: suspend CoroutineScope.(T) -> Unit = {},
workDispatcher: CoroutineDispatcher = AppDispatcherImpl.default,
eachDispatcher: CoroutineDispatcher = AppDispatcherImpl.main.immediate,
action: suspend CoroutineScope.() -> T,
): Job

/**
* Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher.
* @param onError - error handler
* @param each - action for each element of the flow
* @return Job
* @see Flow
* @see Job
* @see AppDispatcher
* */
fun <T> Flow<T>.launch(
onError: suspend (cause: Throwable) -> Unit = {},
workDispatcher: CoroutineDispatcher = AppDispatcherImpl.default,
eachDispatcher: CoroutineDispatcher = AppDispatcherImpl.main.immediate,
each: suspend (T) -> Unit
): Job
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.stslex.wizard.core.ui.mvi.v2

import com.stslex.wizard.core.ui.mvi.Store
import com.stslex.wizard.core.ui.mvi.Store.Action
import com.stslex.wizard.core.ui.mvi.Store.Event
import com.stslex.wizard.core.ui.mvi.Store.State

@Suppress("UNCHECKED_CAST")
fun <S : State, A : Action, E : Event, TStore : Store<S, A, E>> store(
initialState: S,
handlers: Set<Handler<S, *, E, A>>
): TStore = BaseStoreImpl(initialState, handlers) as TStore

fun <S : State, A : StoreAction, E : Event, StoreAction : Action> Handler<S, A, E, StoreAction>.invoke(
store: HandlerStore<S, StoreAction, E>,
action: A
) {
with(store) {
this.invoke(action)
}
}

inline fun <S : State, reified A : StoreAction, E : Event, StoreAction : Action> handler(
crossinline block: HandlerStore<S, StoreAction, E>.(action: A) -> Unit
) = object : Handler<S, A, E, StoreAction>(A::class) {

override fun HandlerStore<S, StoreAction, E>.invoke(action: A) {
block(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import com.stslex.wizard.feature.profile.data.repository.ProfileRepository
import com.stslex.wizard.feature.profile.data.repository.ProfileRepositoryImpl
import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor
import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractorImpl
import com.stslex.wizard.feature.profile.navigation.ProfileRouter
import com.stslex.wizard.feature.profile.navigation.ProfileRouterImpl
import com.stslex.wizard.feature.profile.ui.store.ProfileStore
import com.stslex.wizard.feature.profile.ui.store.ProfileStoreImpl
import org.koin.core.annotation.Module
Expand All @@ -20,7 +18,6 @@ class ModuleFeatureProfile : AppModule() {

override fun declaration(): ModuleDeclaration = {
storeOf(::ProfileStoreImpl) { bind<ProfileStore>() }
factoryOf(::ProfileRouterImpl) { bind<ProfileRouter>() }
factoryOf(::ProfileInteractorImpl) { bind<ProfileInteractor>() }
factoryOf(::ProfileRepositoryImpl) { bind<ProfileRepository>() }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.stslex.wizard.feature.profile.mvi

import com.stslex.wizard.core.ui.mvi.v2.Handler
import com.stslex.wizard.core.ui.mvi.v2.HandlerStore
import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action
import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action.Click
import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event
import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State

class ClickersHandler : Handler<State, Click, Event, Action>(Click::class) {

override fun HandlerStore<State, Action, Event>.invoke(action: Click) {
when (action) {
Click.BackButtonClick -> sendAction(Action.Navigation.Back)
Click.FavouriteClick -> sendAction(Action.Navigation.Favourite(state.value.uuid))
Click.FollowersClick -> sendAction(Action.Navigation.Followers(state.value.uuid))
Click.FollowingClick -> sendAction(Action.Navigation.Following(state.value.uuid))
Click.SettingsClick -> sendAction(Action.Navigation.Settings)
}
}
}
Loading

0 comments on commit bd4f3d2

Please sign in to comment.