diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt index e84d5da..0bcc85e 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt @@ -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 ComponentActivity.viewModel( noinline factory: () -> VM @@ -17,3 +21,53 @@ inline fun ComponentActivity.viewModel( } return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise }) } + + +inline fun ComponentActivity.state( + noinline factory: () -> StateViewModel +): Lazy> { + val factoryPromise = object : Factory { + override fun create(modelClass: Class): 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> +} + +class FooViewModelLazy @JvmOverloads constructor( + private val key: String, + private val viewModelClass: KClass, + private val storeProducer: () -> ViewModelStore, + private val factoryProducer: () -> ViewModelProvider.Factory, + private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty } +) : Lazy { + 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 +} \ No newline at end of file diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt index 66fa52c..2c01997 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt @@ -16,11 +16,11 @@ import kotlinx.coroutines.launch class StateViewModel( reducerFactory: ReducerFactory, eventSource: MutableSharedFlow, -) : ViewModel(), StateStore { +) : ViewModel(), State { private val store: Store = createStore(reducerFactory, viewModelScope) override val events: SharedFlow = eventSource - override val state + override val current get() = _state!! private var _state: S by mutableStateOf(store.getState()) @@ -42,8 +42,8 @@ fun createStateViewModel(block: (suspend (E) -> Unit) -> ReducerFactory { +interface State { fun dispatch(action: Action) val events: SharedFlow - val state: S + val current: S } diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt index 5fd36c2..2226172 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt @@ -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) { +private fun DirectoryState.ObserveEvents(listState: LazyListState, toolbarPosition: MutableState) { val context = LocalContext.current StartObserving { this@ObserveEvents.events.launch { diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt index 0154ccb..6e3bd6f 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt @@ -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( diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryAction.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryAction.kt new file mode 100644 index 0000000..3392f36 --- /dev/null +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryAction.kt @@ -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 +} diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryReducer.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt similarity index 73% rename from features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryReducer.kt rename to features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt index e278b53..f01e74f 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryReducer.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt @@ -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 - 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 -} diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt similarity index 75% rename from features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt rename to features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt index 0dd1a41..3f20567 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryState.kt @@ -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 +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 } - diff --git a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt index 4b0b5a8..bc71e31 100644 --- a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt +++ b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt @@ -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(), diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt index f132898..653579a 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt @@ -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(), diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 75f14ef..9bf76da 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -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, diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index f52e00e..455f98f 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -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().directoryViewModel() } + private val directoryViewModel: DirectoryState by state { module().directoryViewModel() } private val loginViewModel by viewModel { module().loginViewModel() } private val profileViewModel by viewModel { module().profileViewModel() } private val homeViewModel by viewModel { module().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }