mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-16 12:10:45 +01:00
save and restoring state view model
This commit is contained in:
parent
de1aa00715
commit
fe363058b5
@ -3,7 +3,11 @@ package app.dapk.st.core
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelLazy
|
import androidx.lifecycle.ViewModelLazy
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.*
|
import androidx.lifecycle.ViewModelProvider.*
|
||||||
|
import androidx.lifecycle.ViewModelStore
|
||||||
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
||||||
noinline factory: () -> VM
|
noinline factory: () -> VM
|
||||||
@ -17,3 +21,53 @@ inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
|||||||
}
|
}
|
||||||
return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise })
|
return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <reified S, E> ComponentActivity.state(
|
||||||
|
noinline factory: () -> StateViewModel<S, E>
|
||||||
|
): Lazy<State<S, E>> {
|
||||||
|
val factoryPromise = object : Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return when(modelClass) {
|
||||||
|
StateViewModel::class.java -> factory() as T
|
||||||
|
else -> throw Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FooViewModelLazy(
|
||||||
|
key = S::class.java.canonicalName!!,
|
||||||
|
StateViewModel::class,
|
||||||
|
{ viewModelStore },
|
||||||
|
{ factoryPromise }
|
||||||
|
) as Lazy<State<S, E>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class FooViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
|
||||||
|
private val key: String,
|
||||||
|
private val viewModelClass: KClass<VM>,
|
||||||
|
private val storeProducer: () -> ViewModelStore,
|
||||||
|
private val factoryProducer: () -> ViewModelProvider.Factory,
|
||||||
|
private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
|
||||||
|
) : Lazy<VM> {
|
||||||
|
private var cached: VM? = null
|
||||||
|
|
||||||
|
override val value: VM
|
||||||
|
get() {
|
||||||
|
val viewModel = cached
|
||||||
|
return if (viewModel == null) {
|
||||||
|
val factory = factoryProducer()
|
||||||
|
val store = storeProducer()
|
||||||
|
ViewModelProvider(
|
||||||
|
store,
|
||||||
|
factory,
|
||||||
|
extrasProducer()
|
||||||
|
).get(key, viewModelClass.java).also {
|
||||||
|
cached = it
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isInitialized(): Boolean = cached != null
|
||||||
|
}
|
@ -16,11 +16,11 @@ import kotlinx.coroutines.launch
|
|||||||
class StateViewModel<S, E>(
|
class StateViewModel<S, E>(
|
||||||
reducerFactory: ReducerFactory<S>,
|
reducerFactory: ReducerFactory<S>,
|
||||||
eventSource: MutableSharedFlow<E>,
|
eventSource: MutableSharedFlow<E>,
|
||||||
) : ViewModel(), StateStore<S, E> {
|
) : ViewModel(), State<S, E> {
|
||||||
|
|
||||||
private val store: Store<S> = createStore(reducerFactory, viewModelScope)
|
private val store: Store<S> = createStore(reducerFactory, viewModelScope)
|
||||||
override val events: SharedFlow<E> = eventSource
|
override val events: SharedFlow<E> = eventSource
|
||||||
override val state
|
override val current
|
||||||
get() = _state!!
|
get() = _state!!
|
||||||
private var _state: S by mutableStateOf(store.getState())
|
private var _state: S by mutableStateOf(store.getState())
|
||||||
|
|
||||||
@ -42,8 +42,8 @@ fun <S, E> createStateViewModel(block: (suspend (E) -> Unit) -> ReducerFactory<S
|
|||||||
return StateViewModel(reducer, eventSource)
|
return StateViewModel(reducer, eventSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateStore<S, E> {
|
interface State<S, E> {
|
||||||
fun dispatch(action: Action)
|
fun dispatch(action: Action)
|
||||||
val events: SharedFlow<E>
|
val events: SharedFlow<E>
|
||||||
val state: S
|
val current: S
|
||||||
}
|
}
|
||||||
|
@ -35,9 +35,13 @@ import app.dapk.st.design.components.CircleishAvatar
|
|||||||
import app.dapk.st.design.components.GenericEmpty
|
import app.dapk.st.design.components.GenericEmpty
|
||||||
import app.dapk.st.design.components.GenericError
|
import app.dapk.st.design.components.GenericError
|
||||||
import app.dapk.st.design.components.Toolbar
|
import app.dapk.st.design.components.Toolbar
|
||||||
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
|
import app.dapk.st.directory.state.DirectoryEvent.OpenDownloadUrl
|
||||||
import app.dapk.st.directory.DirectoryScreenState.Content
|
import app.dapk.st.directory.state.DirectoryScreenState.Content
|
||||||
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
|
import app.dapk.st.directory.state.DirectoryScreenState.EmptyLoading
|
||||||
|
import app.dapk.st.directory.state.ComponentLifecycle
|
||||||
|
import app.dapk.st.directory.state.DirectoryEvent
|
||||||
|
import app.dapk.st.directory.state.DirectoryScreenState
|
||||||
|
import app.dapk.st.directory.state.DirectoryState
|
||||||
import app.dapk.st.engine.DirectoryItem
|
import app.dapk.st.engine.DirectoryItem
|
||||||
import app.dapk.st.engine.RoomOverview
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.engine.Typing
|
import app.dapk.st.engine.Typing
|
||||||
@ -53,8 +57,8 @@ import java.time.temporal.ChronoUnit
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
|
fun DirectoryScreen(directoryViewModel: DirectoryState) {
|
||||||
val state = directoryViewModel.state
|
val state = directoryViewModel.current
|
||||||
|
|
||||||
val listState: LazyListState = rememberLazyListState(
|
val listState: LazyListState = rememberLazyListState(
|
||||||
initialFirstVisibleItemIndex = 0,
|
initialFirstVisibleItemIndex = 0,
|
||||||
@ -101,7 +105,7 @@ fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DirectoryViewModel.ObserveEvents(listState: LazyListState, toolbarPosition: MutableState<Float>) {
|
private fun DirectoryState.ObserveEvents(listState: LazyListState, toolbarPosition: MutableState<Float>) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
StartObserving {
|
StartObserving {
|
||||||
this@ObserveEvents.events.launch {
|
this@ObserveEvents.events.launch {
|
||||||
|
@ -4,6 +4,9 @@ import android.content.Context
|
|||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.core.StateViewModel
|
import app.dapk.st.core.StateViewModel
|
||||||
import app.dapk.st.core.createStateViewModel
|
import app.dapk.st.core.createStateViewModel
|
||||||
|
import app.dapk.st.directory.state.DirectoryEvent
|
||||||
|
import app.dapk.st.directory.state.DirectoryScreenState
|
||||||
|
import app.dapk.st.directory.state.directoryReducer
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
|
||||||
class DirectoryModule(
|
class DirectoryModule(
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package app.dapk.st.directory.state
|
||||||
|
|
||||||
|
import app.dapk.st.engine.DirectoryState
|
||||||
|
import app.dapk.state.Action
|
||||||
|
|
||||||
|
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,15 +1,12 @@
|
|||||||
package app.dapk.st.directory
|
package app.dapk.st.directory.state
|
||||||
|
|
||||||
import app.dapk.st.core.StateStore
|
import app.dapk.st.directory.ShortcutHandler
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.engine.DirectoryState
|
|
||||||
import app.dapk.state.*
|
import app.dapk.state.*
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
typealias DirectoryViewModel = StateStore<DirectoryScreenState, DirectoryEvent>
|
|
||||||
|
|
||||||
fun directoryReducer(
|
fun directoryReducer(
|
||||||
chatEngine: ChatEngine,
|
chatEngine: ChatEngine,
|
||||||
shortcutHandler: ShortcutHandler,
|
shortcutHandler: ShortcutHandler,
|
||||||
@ -18,6 +15,7 @@ fun directoryReducer(
|
|||||||
var syncJob: Job? = null
|
var syncJob: Job? = null
|
||||||
return createReducer(
|
return createReducer(
|
||||||
initialState = DirectoryScreenState.EmptyLoading,
|
initialState = DirectoryScreenState.EmptyLoading,
|
||||||
|
|
||||||
multi(ComponentLifecycle::class) { action ->
|
multi(ComponentLifecycle::class) { action ->
|
||||||
when (action) {
|
when (action) {
|
||||||
ComponentLifecycle.OnVisible -> async { _ ->
|
ComponentLifecycle.OnVisible -> async { _ ->
|
||||||
@ -33,28 +31,16 @@ fun directoryReducer(
|
|||||||
ComponentLifecycle.OnGone -> sideEffect { _, _ -> syncJob?.cancel() }
|
ComponentLifecycle.OnGone -> sideEffect { _, _ -> syncJob?.cancel() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
change(DirectoryStateChange::class) { action, _ ->
|
change(DirectoryStateChange::class) { action, _ ->
|
||||||
when (action) {
|
when (action) {
|
||||||
is DirectoryStateChange.Content -> DirectoryScreenState.Content(action.content)
|
is DirectoryStateChange.Content -> DirectoryScreenState.Content(action.content)
|
||||||
DirectoryStateChange.Empty -> DirectoryScreenState.Empty
|
DirectoryStateChange.Empty -> DirectoryScreenState.Empty
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sideEffect(DirectorySideEffect.ScrollToTop::class) { _, _ ->
|
sideEffect(DirectorySideEffect.ScrollToTop::class) { _, _ ->
|
||||||
eventEmitter(DirectoryEvent.ScrollToTop)
|
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,9 +1,11 @@
|
|||||||
package app.dapk.st.directory
|
package app.dapk.st.directory.state
|
||||||
|
|
||||||
|
import app.dapk.st.core.State
|
||||||
import app.dapk.st.engine.DirectoryState
|
import app.dapk.st.engine.DirectoryState
|
||||||
|
|
||||||
sealed interface DirectoryScreenState {
|
typealias DirectoryState = State<DirectoryScreenState, DirectoryEvent>
|
||||||
|
|
||||||
|
sealed interface DirectoryScreenState {
|
||||||
object EmptyLoading : DirectoryScreenState
|
object EmptyLoading : DirectoryScreenState
|
||||||
object Empty : DirectoryScreenState
|
object Empty : DirectoryScreenState
|
||||||
data class Content(
|
data class Content(
|
||||||
@ -15,4 +17,3 @@ sealed interface DirectoryEvent {
|
|||||||
data class OpenDownloadUrl(val url: String) : DirectoryEvent
|
data class OpenDownloadUrl(val url: String) : DirectoryEvent
|
||||||
object ScrollToTop : DirectoryEvent
|
object ScrollToTop : DirectoryEvent
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
|||||||
package app.dapk.st.directory
|
package app.dapk.st.directory
|
||||||
|
|
||||||
import ViewModelTest
|
import ViewModelTest
|
||||||
|
import app.dapk.st.directory.state.DirectoryScreenState
|
||||||
|
import app.dapk.st.directory.state.DirectoryState
|
||||||
import app.dapk.st.engine.DirectoryItem
|
import app.dapk.st.engine.DirectoryItem
|
||||||
import app.dapk.st.engine.UnreadCount
|
import app.dapk.st.engine.UnreadCount
|
||||||
import fake.FakeChatEngine
|
import fake.FakeChatEngine
|
||||||
@ -18,7 +20,7 @@ class DirectoryViewModelTest {
|
|||||||
private val fakeShortcutHandler = FakeShortcutHandler()
|
private val fakeShortcutHandler = FakeShortcutHandler()
|
||||||
private val fakeChatEngine = FakeChatEngine()
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
|
||||||
private val viewModel = DirectoryViewModel(
|
private val viewModel = DirectoryState(
|
||||||
fakeShortcutHandler.instance,
|
fakeShortcutHandler.instance,
|
||||||
fakeChatEngine,
|
fakeChatEngine,
|
||||||
runViewModelTest.testMutableStateFactory(),
|
runViewModelTest.testMutableStateFactory(),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package app.dapk.st.home
|
package app.dapk.st.home
|
||||||
|
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.directory.DirectoryViewModel
|
import app.dapk.st.directory.state.DirectoryState
|
||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.login.LoginViewModel
|
import app.dapk.st.login.LoginViewModel
|
||||||
@ -13,7 +13,7 @@ class HomeModule(
|
|||||||
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
||||||
return HomeViewModel(
|
return HomeViewModel(
|
||||||
chatEngine,
|
chatEngine,
|
||||||
storeModule.credentialsStore(),
|
storeModule.credentialsStore(),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package app.dapk.st.home
|
package app.dapk.st.home
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.directory.DirectorySideEffect
|
import app.dapk.st.directory.state.DirectorySideEffect
|
||||||
import app.dapk.st.directory.DirectoryViewModel
|
import app.dapk.st.directory.state.DirectoryState
|
||||||
import app.dapk.st.domain.StoreCleaner
|
import app.dapk.st.domain.StoreCleaner
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.home.HomeScreenState.*
|
import app.dapk.st.home.HomeScreenState.*
|
||||||
@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
|
|||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
private val chatEngine: ChatEngine,
|
private val chatEngine: ChatEngine,
|
||||||
private val credentialsProvider: CredentialsStore,
|
private val credentialsProvider: CredentialsStore,
|
||||||
private val directoryViewModel: DirectoryViewModel,
|
private val directoryViewModel: DirectoryState,
|
||||||
private val loginViewModel: LoginViewModel,
|
private val loginViewModel: LoginViewModel,
|
||||||
private val profileViewModel: ProfileViewModel,
|
private val profileViewModel: ProfileViewModel,
|
||||||
private val cacheCleaner: StoreCleaner,
|
private val cacheCleaner: StoreCleaner,
|
||||||
|
@ -14,8 +14,9 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import app.dapk.st.core.DapkActivity
|
import app.dapk.st.core.DapkActivity
|
||||||
import app.dapk.st.core.module
|
import app.dapk.st.core.module
|
||||||
import app.dapk.st.core.viewModel
|
import app.dapk.st.core.viewModel
|
||||||
|
import app.dapk.st.core.state
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
import app.dapk.st.directory.DirectoryViewModel
|
import app.dapk.st.directory.state.DirectoryState
|
||||||
import app.dapk.st.login.LoginModule
|
import app.dapk.st.login.LoginModule
|
||||||
import app.dapk.st.profile.ProfileModule
|
import app.dapk.st.profile.ProfileModule
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -23,7 +24,7 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
|
|
||||||
class MainActivity : DapkActivity() {
|
class MainActivity : DapkActivity() {
|
||||||
|
|
||||||
private val directoryViewModel: DirectoryViewModel by viewModel { module<DirectoryModule>().directoryViewModel() }
|
private val directoryViewModel: DirectoryState by state { module<DirectoryModule>().directoryViewModel() }
|
||||||
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
|
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
|
||||||
private val profileViewModel by viewModel { module<ProfileModule>().profileViewModel() }
|
private val profileViewModel by viewModel { module<ProfileModule>().profileViewModel() }
|
||||||
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }
|
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user