Skip to content

Commit

Permalink
Merge pull request #51 from stslex/dev
Browse files Browse the repository at this point in the history
Refactor mvi arch
  • Loading branch information
stslex authored Dec 15, 2024
2 parents 4ef6b53 + 5ea63d9 commit 1f67b39
Show file tree
Hide file tree
Showing 22 changed files with 494 additions and 188 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
@@ -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,114 @@
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, HStore : HandlerStore<S, A, E>>(
initialState: S,
private val handlerCreator: HandlerCreator<S, A, E, HStore>,
) : 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 = handlerCreator(action) as Handler<A, HStore>
handler.invoke(this as HStore, 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, HStore : HandlerStore<S, A, E>>(
initialState: S,
handlerCreator: HandlerCreator<S, A, E, HStore>,
) : BaseStore<S, A, E, HStore>(
initialState = initialState,
handlerCreator = handlerCreator
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stslex.wizard.core.ui.mvi.v2

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

fun interface Handler<A : Action, TStore : HandlerStore<*, *, *>> {

fun TStore.invoke(action: A)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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

fun interface HandlerCreator<S : State, A : Action, E : Event, HStore : HandlerStore<S, A, E>> {

operator fun invoke(action: A): Handler<*, HStore>
}
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,25 @@
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>, HStore : HandlerStore<S, A, E>> store(
initialState: S,
handlerCreator: HandlerCreator<S, A, E, HStore>
): TStore = BaseStoreImpl(initialState, handlerCreator) as TStore

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

inline fun <S : State, A : Action, E : Event, HStore : HandlerStore<S, A, E>> handler(
crossinline block: HandlerStore<S, A, E>.(action: A) -> Unit
) = Handler<A, HStore> { block(it) }
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ 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.mvi.ClickersHandler
import com.stslex.wizard.feature.profile.mvi.InitStorageHandler
import com.stslex.wizard.feature.profile.mvi.LogoutHandler
import com.stslex.wizard.feature.profile.mvi.NavigationHandler
import com.stslex.wizard.feature.profile.mvi.RepeatLastActionHandler
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 @@ -19,8 +22,14 @@ import org.koin.dsl.ModuleDeclaration
class ModuleFeatureProfile : AppModule() {

override fun declaration(): ModuleDeclaration = {
factoryOf(::InitStorageHandler)
factoryOf(::LogoutHandler)
factoryOf(::RepeatLastActionHandler)
factoryOf(::ClickersHandler)
factoryOf(::NavigationHandler)

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,18 @@
package com.stslex.wizard.feature.profile.mvi

import com.stslex.wizard.core.ui.mvi.v2.Handler
import com.stslex.wizard.feature.profile.ui.store.ProfileHandlerStore
import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action

class ClickersHandler : Handler<Action.Click, ProfileHandlerStore> {

override fun ProfileHandlerStore.invoke(action: Action.Click) {
when (action) {
Action.Click.BackButtonClick -> sendAction(Action.Navigation.Back)
Action.Click.FavouriteClick -> sendAction(Action.Navigation.Favourite(state.value.uuid))
Action.Click.FollowersClick -> sendAction(Action.Navigation.Followers(state.value.uuid))
Action.Click.FollowingClick -> sendAction(Action.Navigation.Following(state.value.uuid))
Action.Click.SettingsClick -> sendAction(Action.Navigation.Settings)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.stslex.wizard.feature.profile.mvi

import androidx.compose.ui.graphics.Color
import com.stslex.wizard.core.database.store.UserStore
import com.stslex.wizard.core.navigation.Screen
import com.stslex.wizard.core.ui.mvi.v2.Handler
import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor
import com.stslex.wizard.feature.profile.ui.model.ProfileAvatarModel
import com.stslex.wizard.feature.profile.ui.model.toUi
import com.stslex.wizard.feature.profile.ui.store.ProfileHandlerStore
import com.stslex.wizard.feature.profile.ui.store.ProfileScreenState
import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action

class InitStorageHandler(
private val interactor: ProfileInteractor,
private val userStore: UserStore,
) : Handler<Action.Init, ProfileHandlerStore> {

override fun ProfileHandlerStore.invoke(action: Action.Init) {
val uuid = action.uuid.ifBlank { userStore.uuid }

updateState { currentState ->
currentState.copy(
isSelf = action.type == Screen.Profile.Type.SELF,
uuid = uuid,
)
}

interactor.getProfile(uuid)
.launch(
onError = { error ->
updateState { currentState ->
currentState.copy(
screen = ProfileScreenState.Error(error)
)
}
}
) { profile ->
val avatar = if (profile.avatarUrl.isBlank()) {
ProfileAvatarModel.Empty(
color = Color.Gray, // TODO replace with random color
symbol = profile.username.firstOrNull()?.lowercase().orEmpty()
)
} else {
ProfileAvatarModel.Content(profile.avatarUrl)
}
val profileUi = profile.toUi(
avatarModel = avatar
)
updateState { currentState ->
currentState.copy(
screen = ProfileScreenState.Content.NotLoading(profileUi)
)
}
}
}
}
Loading

0 comments on commit 1f67b39

Please sign in to comment.