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.lifecycle.ViewModel
import androidx.lifecycle.ViewModelLazy
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(
noinline factory: () -> VM
@ -17,3 +21,53 @@ inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
}
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>(
reducerFactory: ReducerFactory<S>,
eventSource: MutableSharedFlow<E>,
) : ViewModel(), StateStore<S, E> {
) : ViewModel(), State<S, E> {
private val store: Store<S> = createStore(reducerFactory, viewModelScope)
override val events: SharedFlow<E> = eventSource
override val state
override val current
get() = _state!!
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)
}
interface StateStore<S, E> {
interface State<S, E> {
fun dispatch(action: Action)
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.GenericError
import app.dapk.st.design.components.Toolbar
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
import app.dapk.st.directory.DirectoryScreenState.Content
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
import app.dapk.st.directory.state.DirectoryEvent.OpenDownloadUrl
import app.dapk.st.directory.state.DirectoryScreenState.Content
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.RoomOverview
import app.dapk.st.engine.Typing
@ -53,8 +57,8 @@ import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
@Composable
fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
val state = directoryViewModel.state
fun DirectoryScreen(directoryViewModel: DirectoryState) {
val state = directoryViewModel.current
val listState: LazyListState = rememberLazyListState(
initialFirstVisibleItemIndex = 0,
@ -101,7 +105,7 @@ fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
}
@Composable
private fun DirectoryViewModel.ObserveEvents(listState: LazyListState, toolbarPosition: MutableState<Float>) {
private fun DirectoryState.ObserveEvents(listState: LazyListState, toolbarPosition: MutableState<Float>) {
val context = LocalContext.current
StartObserving {
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.StateViewModel
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
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.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,
@ -18,6 +15,7 @@ fun directoryReducer(
var syncJob: Job? = null
return createReducer(
initialState = DirectoryScreenState.EmptyLoading,
multi(ComponentLifecycle::class) { action ->
when (action) {
ComponentLifecycle.OnVisible -> async { _ ->
@ -33,28 +31,16 @@ fun directoryReducer(
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
}

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
sealed interface DirectoryScreenState {
typealias DirectoryState = State<DirectoryScreenState, DirectoryEvent>
sealed interface DirectoryScreenState {
object EmptyLoading : DirectoryScreenState
object Empty : DirectoryScreenState
data class Content(
@ -15,4 +17,3 @@ sealed interface DirectoryEvent {
data class OpenDownloadUrl(val url: String) : DirectoryEvent
object ScrollToTop : DirectoryEvent
}

View File

@ -1,6 +1,8 @@
package app.dapk.st.directory
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.UnreadCount
import fake.FakeChatEngine
@ -18,7 +20,7 @@ class DirectoryViewModelTest {
private val fakeShortcutHandler = FakeShortcutHandler()
private val fakeChatEngine = FakeChatEngine()
private val viewModel = DirectoryViewModel(
private val viewModel = DirectoryState(
fakeShortcutHandler.instance,
fakeChatEngine,
runViewModelTest.testMutableStateFactory(),

View File

@ -1,7 +1,7 @@
package app.dapk.st.home
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.engine.ChatEngine
import app.dapk.st.login.LoginViewModel
@ -13,7 +13,7 @@ class HomeModule(
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
) : ProvidableModule {
fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
return HomeViewModel(
chatEngine,
storeModule.credentialsStore(),

View File

@ -1,8 +1,8 @@
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.directory.state.DirectorySideEffect
import app.dapk.st.directory.state.DirectoryState
import app.dapk.st.domain.StoreCleaner
import app.dapk.st.engine.ChatEngine
import app.dapk.st.home.HomeScreenState.*
@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
class HomeViewModel(
private val chatEngine: ChatEngine,
private val credentialsProvider: CredentialsStore,
private val directoryViewModel: DirectoryViewModel,
private val directoryViewModel: DirectoryState,
private val loginViewModel: LoginViewModel,
private val profileViewModel: ProfileViewModel,
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.module
import app.dapk.st.core.viewModel
import app.dapk.st.core.state
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.profile.ProfileModule
import kotlinx.coroutines.flow.launchIn
@ -23,7 +24,7 @@ import kotlinx.coroutines.flow.onEach
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 profileViewModel by viewModel { module<ProfileModule>().profileViewModel() }
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }