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

Refactor mvi arch #51

Merged
merged 5 commits into from
Dec 15, 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
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
Loading