Last active
November 3, 2025 11:56
-
-
Save Andrew0000/3e242c59dae9ec8275b6008a5879a007 to your computer and use it in GitHub Desktop.
AppViewModelStore - ViewModel scope (lifecycle) management for Compose Multiplatform
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import androidx.compose.runtime.Composable | |
| import androidx.lifecycle.ViewModel | |
| import androidx.lifecycle.ViewModelStore | |
| import androidx.lifecycle.ViewModelStoreOwner | |
| import kotlinx.coroutines.CoroutineScope | |
| import kotlinx.coroutines.Dispatchers | |
| import kotlinx.coroutines.Job | |
| import kotlinx.coroutines.SupervisorJob | |
| import kotlinx.coroutines.launch | |
| import kotlinx.coroutines.yield | |
| import org.koin.core.parameter.ParametersDefinition | |
| /** | |
| * Manages [ViewModel] scopes for [Composable] screens. | |
| * It allows ViewModels to survive a configuration change (rotation) | |
| * by postponing the actual deletion to a moment after the configuration change. | |
| * If, after the configuration change, the ViewModel is requested, then the deletion is cancelled. | |
| * | |
| * This is a simple alternative for default [androidx.lifecycle.ViewModelStoreOwner] | |
| * which is bound to Activity. | |
| * Also, this solution is not bound to navigation and/or specific libraries. | |
| * | |
| * For example, the approach where NavHost controls the scope of ViewModels | |
| * is bound to the Navigation 2 library. | |
| * | |
| * Useful links: | |
| * | |
| * https://developer.android.com/topic/libraries/architecture/viewmodel#jetpack-compose | |
| * | |
| * https://proandroiddev.com/composable-scoped-viewmodel-an-interesting-experiment-b982b86d84cd | |
| * | |
| * Tested on Android 15, Android 8, iOS 16. | |
| */ | |
| object AppViewModelStore { | |
| val vms = mutableMapOf<String, AppViewModel>() | |
| val vmsToDelete = mutableMapOf<String, Job>() | |
| val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) | |
| /** | |
| * Get (create if needed) a ViewModel's instance. | |
| * | |
| * Must be called from main thread. | |
| */ | |
| inline fun <reified T: AppViewModel> get(key: Any?, noinline parameters: ParametersDefinition? = null): T { | |
| val fullKey = getFullKey<T>(key) | |
| vmsToDelete[fullKey]?.cancel() | |
| var vm = vms[fullKey] as T? | |
| Log.d("[AppViewModelStore] get: $vm, size: ${vms.size}") | |
| if (vm == null) { | |
| vm = Di.koin.get<T>(parameters = parameters) | |
| vms[fullKey] = vm | |
| Log.d("[AppViewModelStore] CREATED: $vm, size: ${vms.size}") | |
| } | |
| return vm | |
| } | |
| /** | |
| * Mark the ViewModel as not used and queue it for deletion. | |
| * | |
| * Must be called from main thread. | |
| */ | |
| inline fun <reified T: AppViewModel> onDisposed(key: Any?) { | |
| val fullKey = getFullKey<T>(key) | |
| vmsToDelete[fullKey]?.cancel() | |
| Log.d("[AppViewModelStore] onDisposed: $fullKey, size: ${vms.size}") | |
| val deletionJob = scope.launch { | |
| // Postpone the deletion to a next frame in the main thread. | |
| yield() | |
| val removedVM = vms.remove(fullKey) | |
| removedVM?.clear() | |
| vmsToDelete.remove(fullKey) | |
| Log.d("[AppViewModelStore] REMOVED: $removedVM, size: ${vms.size}") | |
| } | |
| vmsToDelete += fullKey to deletionJob | |
| } | |
| inline fun <reified T: ViewModel> getFullKey(key: Any?) = | |
| T::class.toString() + key | |
| } | |
| /** | |
| * A [ViewModel] with public [clear] function. | |
| */ | |
| open class AppViewModel : ViewModel() { | |
| private val viewModelStoreOwner: ViewModelStoreOwner = object : ViewModelStoreOwner { | |
| override val viewModelStore = ViewModelStore().also { | |
| it.put("", this@AppViewModel) | |
| } | |
| } | |
| fun clear() { | |
| viewModelStoreOwner.viewModelStore.clear() | |
| } | |
| } | |
| @Composable | |
| inline fun <reified T: AppViewModel> appViewModel( | |
| key: Any? = null, | |
| noinline parameters: ParametersDefinition? = null, | |
| ): T { | |
| val vm = remember { | |
| AppViewModelStore.get<T>(key, parameters) | |
| } | |
| DisposableEffect(Unit) { | |
| onDispose { | |
| AppViewModelStore.onDisposed<T>(key) | |
| } | |
| } | |
| return vm | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment