wip
This commit is contained in:
parent
58730470f6
commit
de1aa00715
|
@ -5,4 +5,5 @@ dependencies {
|
|||
implementation project(":features:navigator")
|
||||
implementation project(":design-library")
|
||||
api project(":domains:android:core")
|
||||
api project(":domains:state")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package app.dapk.st.core
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.state.Action
|
||||
import app.dapk.state.ReducerFactory
|
||||
import app.dapk.state.Store
|
||||
import app.dapk.state.createStore
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class StateViewModel<S, E>(
|
||||
reducerFactory: ReducerFactory<S>,
|
||||
eventSource: MutableSharedFlow<E>,
|
||||
) : ViewModel(), StateStore<S, E> {
|
||||
|
||||
private val store: Store<S> = createStore(reducerFactory, viewModelScope)
|
||||
override val events: SharedFlow<E> = eventSource
|
||||
override val state
|
||||
get() = _state!!
|
||||
private var _state: S by mutableStateOf(store.getState())
|
||||
|
||||
init {
|
||||
_state = store.getState()
|
||||
store.subscribe {
|
||||
_state = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatch(action: Action) {
|
||||
viewModelScope.launch { store.dispatch(action) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <S, E> createStateViewModel(block: (suspend (E) -> Unit) -> ReducerFactory<S>): StateViewModel<S, E> {
|
||||
val eventSource = MutableSharedFlow<E>(extraBufferCapacity = 1)
|
||||
val reducer = block { eventSource.emit(it) }
|
||||
return StateViewModel(reducer, eventSource)
|
||||
}
|
||||
|
||||
interface StateStore<S, E> {
|
||||
fun dispatch(action: Action)
|
||||
val events: SharedFlow<E>
|
||||
val state: S
|
||||
}
|
|
@ -25,4 +25,3 @@ abstract class DapkViewModel<S, VE>(initialState: S, factory: MutableStateFactor
|
|||
state = reducer(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
plugins {
|
||||
id 'kotlin'
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package app.dapk.state
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun <S> createStore(reducerFactory: ReducerFactory<S>, coroutineScope: CoroutineScope): Store<S> {
|
||||
val subscribers = mutableListOf<(S) -> Unit>()
|
||||
var state: S = reducerFactory.initialState()
|
||||
return object : Store<S> {
|
||||
private val scope = createScope(coroutineScope, this)
|
||||
private val reducer = reducerFactory.create(scope)
|
||||
|
||||
override suspend fun dispatch(action: Action) {
|
||||
scope.coroutineScope.launch {
|
||||
state = reducer.reduce(action).also { nextState ->
|
||||
println("!!! next state: $nextState")
|
||||
subscribers.forEach { it.invoke(nextState) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState() = state
|
||||
|
||||
override fun subscribe(subscriber: (S) -> Unit) {
|
||||
subscribers.add(subscriber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ReducerFactory<S> {
|
||||
fun create(scope: ReducerScope<S>): Reducer<S>
|
||||
fun initialState(): S
|
||||
}
|
||||
|
||||
fun interface Reducer<S> {
|
||||
suspend fun reduce(action: Action): S
|
||||
}
|
||||
|
||||
private fun <S> createScope(coroutineScope: CoroutineScope, store: Store<S>) = object : ReducerScope<S> {
|
||||
override val coroutineScope = coroutineScope
|
||||
override suspend fun dispatch(action: Action) = store.dispatch(action)
|
||||
override fun getState(): S = store.getState()
|
||||
}
|
||||
|
||||
interface Store<S> {
|
||||
suspend fun dispatch(action: Action)
|
||||
fun getState(): S
|
||||
fun subscribe(subscriber: (S) -> Unit)
|
||||
}
|
||||
|
||||
interface ReducerScope<S> {
|
||||
val coroutineScope: CoroutineScope
|
||||
suspend fun dispatch(action: Action)
|
||||
fun getState(): S
|
||||
}
|
||||
|
||||
sealed interface ActionHandler<S> {
|
||||
val key: KClass<Action>
|
||||
|
||||
class Async<S>(override val key: KClass<Action>, val handler: suspend ReducerScope<S>.(Action) -> Unit) : ActionHandler<S>
|
||||
class Sync<S>(override val key: KClass<Action>, val handler: (Action, S) -> S) : ActionHandler<S>
|
||||
class Delegate<S>(override val key: KClass<Action>, val handler: ReducerScope<S>.(Action) -> ActionHandler<S>) : ActionHandler<S>
|
||||
}
|
||||
|
||||
fun <S> createReducer(
|
||||
initialState: S,
|
||||
vararg reducers: (ReducerScope<S>) -> ActionHandler<S>,
|
||||
): ReducerFactory<S> {
|
||||
return object : ReducerFactory<S> {
|
||||
override fun create(scope: ReducerScope<S>): Reducer<S> {
|
||||
val reducersMap = reducers
|
||||
.map { it.invoke(scope) }
|
||||
.groupBy { it.key }
|
||||
|
||||
return Reducer { action ->
|
||||
val result = reducersMap.keys
|
||||
.filter { it.java.isAssignableFrom(action::class.java) }
|
||||
.fold(scope.getState() ?: initialState) { acc, key ->
|
||||
val actionHandlers = reducersMap[key]!!
|
||||
actionHandlers.fold(acc) { acc, handler ->
|
||||
when (handler) {
|
||||
is ActionHandler.Async -> {
|
||||
handler.handler.invoke(scope, action)
|
||||
acc
|
||||
}
|
||||
|
||||
is ActionHandler.Sync -> handler.handler.invoke(action, acc)
|
||||
is ActionHandler.Delegate -> when (val next = handler.handler.invoke(scope, action)) {
|
||||
is ActionHandler.Async -> {
|
||||
next.handler.invoke(scope, action)
|
||||
acc
|
||||
}
|
||||
|
||||
is ActionHandler.Sync -> next.handler.invoke(action, acc)
|
||||
is ActionHandler.Delegate -> error("is not possible")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
override fun initialState(): S = initialState
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun <A : Action, S> sideEffect(klass: KClass<A>, block: suspend (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> {
|
||||
return {
|
||||
ActionHandler.Async(key = klass as KClass<Action>) { action -> block(action as A, getState()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <A : Action, S> change(klass: KClass<A>, block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> {
|
||||
return {
|
||||
ActionHandler.Sync(key = klass as KClass<Action>, block as (Action, S) -> S)
|
||||
}
|
||||
}
|
||||
|
||||
fun <A : Action, S> async(klass: KClass<A>, block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> {
|
||||
return {
|
||||
ActionHandler.Async(key = klass as KClass<Action>, block as suspend ReducerScope<S>.(Action) -> Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerScope<S>) -> ActionHandler<S>): (ReducerScope<S>) -> ActionHandler<S> {
|
||||
val multiScope = object : Multi<A, S> {
|
||||
override fun sideEffect(block: (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = sideEffect(klass, block)
|
||||
override fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> = change(klass, block)
|
||||
override fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = async(klass, block)
|
||||
}
|
||||
|
||||
return {
|
||||
ActionHandler.Delegate(key = klass as KClass<Action>) { action ->
|
||||
block(multiScope, action as A).invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Multi<A : Action, S> {
|
||||
fun sideEffect(block: (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
||||
fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S>
|
||||
fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
||||
}
|
||||
|
||||
interface Action
|
|
@ -4,6 +4,7 @@ dependencies {
|
|||
implementation project(":chat-engine")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":domains:state")
|
||||
implementation project(":features:messenger")
|
||||
implementation project(":core")
|
||||
implementation project(":design-library")
|
||||
|
|
|
@ -68,8 +68,8 @@ fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
|
|||
directoryViewModel.ObserveEvents(listState, toolbarOffsetHeightPx)
|
||||
|
||||
LifecycleEffect(
|
||||
onStart = { directoryViewModel.start() },
|
||||
onStop = { directoryViewModel.stop() }
|
||||
onStart = { directoryViewModel.dispatch(ComponentLifecycle.OnVisible) },
|
||||
onStop = { directoryViewModel.dispatch(ComponentLifecycle.OnGone) }
|
||||
)
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
|
|
|
@ -2,6 +2,8 @@ package app.dapk.st.directory
|
|||
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.StateViewModel
|
||||
import app.dapk.st.core.createStateViewModel
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
|
||||
class DirectoryModule(
|
||||
|
@ -9,10 +11,7 @@ class DirectoryModule(
|
|||
private val chatEngine: ChatEngine,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun directoryViewModel(): DirectoryViewModel {
|
||||
return DirectoryViewModel(
|
||||
ShortcutHandler(context),
|
||||
chatEngine,
|
||||
)
|
||||
fun directoryViewModel(): StateViewModel<DirectoryScreenState, DirectoryEvent> {
|
||||
return createStateViewModel { directoryReducer(chatEngine, ShortcutHandler(context), it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package app.dapk.st.directory
|
||||
|
||||
import app.dapk.st.core.StateStore
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.DirectoryState
|
||||
import app.dapk.state.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
typealias DirectoryViewModel = StateStore<DirectoryScreenState, DirectoryEvent>
|
||||
|
||||
fun directoryReducer(
|
||||
chatEngine: ChatEngine,
|
||||
shortcutHandler: ShortcutHandler,
|
||||
eventEmitter: suspend (DirectoryEvent) -> Unit,
|
||||
): ReducerFactory<DirectoryScreenState> {
|
||||
var syncJob: Job? = null
|
||||
return createReducer(
|
||||
initialState = DirectoryScreenState.EmptyLoading,
|
||||
multi(ComponentLifecycle::class) { action ->
|
||||
when (action) {
|
||||
ComponentLifecycle.OnVisible -> async { _ ->
|
||||
syncJob = chatEngine.directory().onEach {
|
||||
shortcutHandler.onDirectoryUpdate(it.map { it.overview })
|
||||
when (it.isEmpty()) {
|
||||
true -> dispatch(DirectoryStateChange.Empty)
|
||||
false -> dispatch(DirectoryStateChange.Content(it))
|
||||
}
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
ComponentLifecycle.OnGone -> sideEffect { _, _ -> syncJob?.cancel() }
|
||||
}
|
||||
},
|
||||
change(DirectoryStateChange::class) { action, _ ->
|
||||
when (action) {
|
||||
is DirectoryStateChange.Content -> DirectoryScreenState.Content(action.content)
|
||||
DirectoryStateChange.Empty -> DirectoryScreenState.Empty
|
||||
}
|
||||
},
|
||||
sideEffect(DirectorySideEffect.ScrollToTop::class) { _, _ ->
|
||||
eventEmitter(DirectoryEvent.ScrollToTop)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface ComponentLifecycle : Action {
|
||||
object OnVisible : ComponentLifecycle
|
||||
object OnGone : ComponentLifecycle
|
||||
}
|
||||
|
||||
sealed interface DirectorySideEffect : Action {
|
||||
object ScrollToTop : DirectorySideEffect
|
||||
}
|
||||
|
||||
sealed interface DirectoryStateChange : Action {
|
||||
object Empty : DirectoryStateChange
|
||||
data class Content(val content: DirectoryState) : DirectoryStateChange
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package app.dapk.st.directory
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.directory.DirectoryScreenState.*
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import app.dapk.st.viewmodel.MutableStateFactory
|
||||
import app.dapk.st.viewmodel.defaultStateFactory
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DirectoryViewModel(
|
||||
private val shortcutHandler: ShortcutHandler,
|
||||
private val chatEngine: ChatEngine,
|
||||
factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(),
|
||||
) : DapkViewModel<DirectoryScreenState, DirectoryEvent>(
|
||||
initialState = EmptyLoading,
|
||||
factory,
|
||||
) {
|
||||
|
||||
private var syncJob: Job? = null
|
||||
|
||||
fun start() {
|
||||
syncJob = viewModelScope.launch {
|
||||
chatEngine.directory().onEach {
|
||||
shortcutHandler.onDirectoryUpdate(it.map { it.overview })
|
||||
state = when (it.isEmpty()) {
|
||||
true -> Empty
|
||||
false -> Content(it)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
syncJob?.cancel()
|
||||
}
|
||||
|
||||
fun scrollToTopOfMessages() {
|
||||
_events.tryEmit(DirectoryEvent.ScrollToTop)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ dependencies {
|
|||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:state')
|
||||
implementation project(":core")
|
||||
implementation project(":design-library")
|
||||
implementation Dependencies.mavenCentral.coil
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.dapk.st.home
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.directory.DirectorySideEffect
|
||||
import app.dapk.st.directory.DirectoryViewModel
|
||||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
|
@ -92,7 +93,7 @@ class HomeViewModel(
|
|||
}
|
||||
|
||||
fun scrollToTopOfMessages() {
|
||||
directoryViewModel.scrollToTopOfMessages()
|
||||
directoryViewModel.dispatch(DirectorySideEffect.ScrollToTop)
|
||||
}
|
||||
|
||||
fun changePage(page: Page) {
|
||||
|
|
|
@ -15,6 +15,7 @@ import app.dapk.st.core.DapkActivity
|
|||
import app.dapk.st.core.module
|
||||
import app.dapk.st.core.viewModel
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.directory.DirectoryViewModel
|
||||
import app.dapk.st.login.LoginModule
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
@ -22,7 +23,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
|
||||
class MainActivity : DapkActivity() {
|
||||
|
||||
private val directoryViewModel by viewModel { module<DirectoryModule>().directoryViewModel() }
|
||||
private val directoryViewModel: DirectoryViewModel by viewModel { module<DirectoryModule>().directoryViewModel() }
|
||||
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
|
||||
private val profileViewModel by viewModel { module<ProfileModule>().profileViewModel() }
|
||||
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package app.dapk.st.messenger
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import org.junit.Test
|
||||
|
||||
class ImplBarTest {
|
||||
|
||||
@Test
|
||||
fun `test bar`() {
|
||||
val viewModelScope = CoroutineScope(UnconfinedTestDispatcher())
|
||||
val reducer = FooBar.createReducer(
|
||||
initialState = ImplBar.BankState(amount = 0),
|
||||
FooBar.sideEffect(ImplBar.BankAction::class) { action, state ->
|
||||
println("SE ${action::class.simpleName} $state")
|
||||
},
|
||||
FooBar.sideEffect(ImplBar.BankAction.Foo::class) { action, state ->
|
||||
println("FOO - $action $state")
|
||||
},
|
||||
FooBar.change(ImplBar.BankAction.Increment::class) { _, state ->
|
||||
state.copy(amount = state.amount + 1)
|
||||
},
|
||||
FooBar.async(ImplBar.BankAction.MultiInc::class) { _ ->
|
||||
flowOf(0, 1, 2)
|
||||
.onEach { dispatch(ImplBar.BankAction.Increment) }
|
||||
.launchIn(coroutineScope)
|
||||
},
|
||||
)
|
||||
|
||||
val store = FooBar.createStore(reducer, viewModelScope)
|
||||
store.subscribe {
|
||||
println(it)
|
||||
}
|
||||
runBlocking { store.dispatch(ImplBar.BankAction.Inc) }
|
||||
}
|
||||
|
||||
}
|
|
@ -33,6 +33,7 @@ include ':domains:android:viewmodel'
|
|||
include ':domains:store'
|
||||
include ':domains:olm-stub'
|
||||
include ':domains:olm'
|
||||
include ':domains:state'
|
||||
|
||||
include ':domains:firebase:crashlytics'
|
||||
include ':domains:firebase:crashlytics-noop'
|
||||
|
|
Loading…
Reference in New Issue