Skip to content

Instantly share code, notes, and snippets.

@Andrew0000
Last active November 3, 2025 11:56
Show Gist options
  • Select an option

  • Save Andrew0000/3e242c59dae9ec8275b6008a5879a007 to your computer and use it in GitHub Desktop.

Select an option

Save Andrew0000/3e242c59dae9ec8275b6008a5879a007 to your computer and use it in GitHub Desktop.
AppViewModelStore - ViewModel scope (lifecycle) management for Compose Multiplatform
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