save and restoring state view model

This commit is contained in:
Adam Brown 2022-10-31 21:06:19 +00:00
parent de1aa00715
commit fe363058b5
11 changed files with 109 additions and 40 deletions

View File

@ -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
}

View File

@ -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
} }

View File

@ -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 {

View File

@ -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(

View File

@ -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
}

View File

@ -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
}

View File

@ -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
} }

View File

@ -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(),

View File

@ -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(),

View File

@ -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,

View File

@ -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) }