Forked from joost-klitsie/RememberViewModelStoreOwner.kt
Created
September 27, 2024 08:07
-
-
Save belinwu/3d93f1824f9f59700a45de6b9e65eaab to your computer and use it in GitHub Desktop.
Revisions
-
joost-klitsie revised this gist
Sep 25, 2024 . 2 changed files with 171 additions and 5 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -15,7 +15,7 @@ fun rememberViewModelStoreOwner( val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel> { ViewModelStoreOwnerViewModel() } val viewModelStoreOwner = viewModelStoreOwnerViewModel.get(viewModelKey, key, localLifecycle) remember { object : RememberObserver { @@ -32,7 +32,7 @@ fun rememberViewModelStoreOwner( } } } return viewModelStoreOwner } @Composable @@ -88,7 +88,7 @@ private class ViewModelStoreOwnerViewModel : ViewModel() { initialLifecycle: Lifecycle, ) { val viewModelStoreOwner: ViewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } @@ -133,13 +133,13 @@ private class ViewModelStoreOwnerViewModel : ViewModel() { fun clear() { supervisorJob.cancelChildren() viewModelStoreOwner.viewModelStore.clear() } fun update(key: Any?, lifecycle: Lifecycle) { if (key != this.key) { this.key = key viewModelStoreOwner.viewModelStore.clear() } attachedLifecycle.update { lifecycle } } 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,166 @@ import androidx.compose.runtime.* import androidx.lifecycle.* import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @Composable fun rememberViewModelStoreOwner( key: Any?, ): ViewModelStoreOwner { val viewModelKey = "rememberViewModelStoreOwner#" + currentCompositeKeyHash.toString(36) val localLifecycle = LocalLifecycleOwner.current.lifecycle val currentViewModelStoreOwner = LocalViewModelStoreOwner.current val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel> { ViewModelStoreOwnerViewModel() } val viewModelStoreOwner = viewModelStoreOwnerViewModel.get(viewModelKey, key, localLifecycle) remember { object : RememberObserver { override fun onAbandoned() { viewModelStoreOwnerViewModel.detachComposable(viewModelKey) } override fun onForgotten() { viewModelStoreOwnerViewModel.detachComposable(viewModelKey) } override fun onRemembered() { viewModelStoreOwnerViewModel.attachComposable(viewModelKey) } } } return remember(currentViewModelStoreOwner, viewModelStoreOwner) { if (currentViewModelStoreOwner is HasDefaultViewModelProviderFactory) { object : ViewModelStoreOwner by viewModelStoreOwner, HasDefaultViewModelProviderFactory by currentViewModelStoreOwner {} } else { viewModelStoreOwner } } } @Composable fun WithViewModelStoreOwner( key: Any?, content: @Composable () -> Unit, ) { CompositionLocalProvider( value = LocalViewModelStoreOwner provides rememberViewModelStoreOwner(key), content = content, ) } @Stable private class ViewModelStoreOwnerViewModel : ViewModel() { private var attachedComposables = mapOf<String, AttachedComposable>() fun get(hashKey: String, key: Any?, lifecycle: Lifecycle) = attachedComposables[hashKey]?.let { it.update(key, lifecycle) it.viewModelStoreOwner } ?: run { val attachedComposable = AttachedComposable(hashKey, key, lifecycle) attachedComposables += hashKey to attachedComposable attachedComposable.viewModelStoreOwner } fun attachComposable(hashKey: String) { attachedComposables[hashKey]?.attachComposable() } fun detachComposable(hashKey: String) { attachedComposables[hashKey]?.detachComposable() } override fun onCleared() { attachedComposables.keys.forEach { dispose(it) } super.onCleared() } private fun dispose(hashKey: String) { attachedComposables[hashKey]?.let { it.clear() attachedComposables -= hashKey } } private inner class AttachedComposable( private val hashKey: String, private var key: Any?, initialLifecycle: Lifecycle, ) { val viewModelStoreOwner: ViewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } private val supervisorJob: CompletableJob = SupervisorJob() private val scope: CoroutineScope = viewModelScope + supervisorJob private val attachedLifecycle = MutableStateFlow<Lifecycle?>(initialLifecycle) private val isAttachedToComposable = MutableSharedFlow<Boolean>() init { scope.launch { attachedLifecycle .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() } .collectLatest { if (it == Lifecycle.Event.ON_DESTROY) { attachedLifecycle.update { null } } } } scope.launch { isAttachedToComposable.collectLatest { isAttached -> when { // If we are attached or we are destroyed, we do not need to do anything isAttached || attachedLifecycle.value == null -> return@collectLatest // If we are detached and the lifecycle state is resumed, we should reset the view model store attachedLifecycle.value?.currentState == Lifecycle.State.RESUMED -> { dispose(hashKey) } else -> { // We wait for the lifecycle event ON_RESUME to be triggered before resetting the ViewModelStore // If in the mean time we are attached again, this work is cancelled attachedLifecycle .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() } // Wait for first event that matches ON_RESUME. .firstOrNull { it == Lifecycle.Event.ON_RESUME } ?: return@collectLatest dispose(hashKey) } } } } } fun clear() { supervisorJob.cancelChildren() viewModelStoreOwner.viewModelStore.clear() } fun update(key: Any?, lifecycle: Lifecycle) { if (key != this.key) { this.key = key viewModelStoreOwner.viewModelStore.clear() } attachedLifecycle.update { lifecycle } } fun attachComposable() { scope.launch { isAttachedToComposable.emit(true) } } fun detachComposable() { scope.launch { isAttachedToComposable.emit(false) } } } } -
joost-klitsie revised this gist
Aug 29, 2024 . 1 changed file with 8 additions and 4 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -77,8 +77,7 @@ private class ViewModelStoreOwnerViewModel : ViewModel() { private fun dispose(hashKey: String) { attachedComposables[hashKey]?.let { it.clear() attachedComposables -= hashKey } } @@ -89,11 +88,11 @@ private class ViewModelStoreOwnerViewModel : ViewModel() { initialLifecycle: Lifecycle, ) { val viewModelStore: ViewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } private val supervisorJob: CompletableJob = SupervisorJob() private val scope: CoroutineScope = viewModelScope + supervisorJob private val attachedLifecycle = MutableStateFlow<Lifecycle?>(initialLifecycle) private val isAttachedToComposable = MutableSharedFlow<Boolean>() @@ -131,6 +130,11 @@ private class ViewModelStoreOwnerViewModel : ViewModel() { } } } fun clear() { supervisorJob.cancelChildren() viewModelStore.viewModelStore.clear() } fun update(key: Any?, lifecycle: Lifecycle) { if (key != this.key) { -
joost-klitsie revised this gist
Aug 28, 2024 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -141,11 +141,11 @@ private class ViewModelStoreOwnerViewModel : ViewModel() { } fun attachComposable() { scope.launch { isAttachedToComposable.emit(true) } } fun detachComposable() { scope.launch { isAttachedToComposable.emit(false) } } } -
joost-klitsie revised this gist
Aug 28, 2024 . 2 changed files with 153 additions and 111 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,153 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.* import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @Composable fun rememberViewModelStoreOwner( key: Any?, ): ViewModelStoreOwner { val viewModelKey = "rememberViewModelStoreOwner#" + currentCompositeKeyHash.toString(36) val localLifecycle = LocalLifecycleOwner.current.lifecycle val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel> { ViewModelStoreOwnerViewModel() } val viewModelStore = viewModelStoreOwnerViewModel.get(viewModelKey, key, localLifecycle) remember { object : RememberObserver { override fun onAbandoned() { viewModelStoreOwnerViewModel.detachComposable(viewModelKey) } override fun onForgotten() { viewModelStoreOwnerViewModel.detachComposable(viewModelKey) } override fun onRemembered() { viewModelStoreOwnerViewModel.attachComposable(viewModelKey) } } } return viewModelStore } @Composable fun WithViewModelStoreOwner( key: Any?, content: @Composable () -> Unit, ) { CompositionLocalProvider( value = LocalViewModelStoreOwner provides rememberViewModelStoreOwner(key), content = content, ) } @Stable private class ViewModelStoreOwnerViewModel : ViewModel() { private var attachedComposables = mapOf<String, AttachedComposable>() fun get(hashKey: String, key: Any?, lifecycle: Lifecycle) = attachedComposables[hashKey]?.let { it.update(key, lifecycle) it.viewModelStore } ?: run { val attachedComposable = AttachedComposable(hashKey, key, lifecycle) attachedComposables += hashKey to attachedComposable attachedComposable.viewModelStore } fun attachComposable(hashKey: String) { attachedComposables[hashKey]?.attachComposable() } fun detachComposable(hashKey: String) { attachedComposables[hashKey]?.detachComposable() } override fun onCleared() { attachedComposables.keys.forEach { dispose(it) } super.onCleared() } private fun dispose(hashKey: String) { attachedComposables[hashKey]?.let { it.viewModelStore.viewModelStore.clear() it.supervisorJob.cancelChildren() attachedComposables -= hashKey } } private inner class AttachedComposable( private val hashKey: String, private var key: Any?, initialLifecycle: Lifecycle, ) { val supervisorJob: CompletableJob = SupervisorJob() val viewModelStore: ViewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } private val scope: CoroutineScope = viewModelScope + supervisorJob private val attachedLifecycle = MutableStateFlow<Lifecycle?>(initialLifecycle) private val isAttachedToComposable = MutableSharedFlow<Boolean>() init { scope.launch { attachedLifecycle .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() } .collectLatest { if (it == Lifecycle.Event.ON_DESTROY) { attachedLifecycle.update { null } } } } scope.launch { isAttachedToComposable.collectLatest { isAttached -> when { // If we are attached or we are destroyed, we do not need to do anything isAttached || attachedLifecycle.value == null -> return@collectLatest // If we are detached and the lifecycle state is resumed, we should reset the view model store attachedLifecycle.value?.currentState == Lifecycle.State.RESUMED -> { dispose(hashKey) } else -> { // We wait for the lifecycle event ON_RESUME to be triggered before resetting the ViewModelStore // If in the mean time we are attached again, this work is cancelled attachedLifecycle .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() } // Wait for first event that matches ON_RESUME. .firstOrNull { it == Lifecycle.Event.ON_RESUME } ?: return@collectLatest dispose(hashKey) } } } } } fun update(key: Any?, lifecycle: Lifecycle) { if (key != this.key) { this.key = key viewModelStore.viewModelStore.clear() } attachedLifecycle.update { lifecycle } } fun attachComposable() { viewModelScope.launch { isAttachedToComposable.emit(true) } } fun detachComposable() { viewModelScope.launch { isAttachedToComposable.emit(false) } } } } 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 charactersOriginal file line number Diff line number Diff line change @@ -1,111 +0,0 @@ -
joost-klitsie renamed this gist
Aug 28, 2024 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
joost-klitsie revised this gist
Aug 28, 2024 . 1 changed file with 13 additions and 7 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -45,29 +45,35 @@ private class ViewModelStoreOwnerViewModel( initialLifecycle: Lifecycle, ) : ViewModel() { private val attachedLifecycle = MutableStateFlow<Lifecycle?>(initialLifecycle) private val isAttachedToComposable = MutableSharedFlow<Boolean>() val viewModelStore: ViewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } init { viewModelScope.launch { attachedLifecycle .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() } .collectLatest { if (it == Lifecycle.Event.ON_DESTROY) { attachedLifecycle.update { null } } } } viewModelScope.launch { isAttachedToComposable.collectLatest { isAttached -> when { // If we are attached or we are destroyed, we do not need to do anything isAttached || attachedLifecycle.value == null -> return@collectLatest // If we are detached and the lifecycle state is resumed, we should reset the view model store attachedLifecycle.value?.currentState == Lifecycle.State.RESUMED -> resetViewModelStore() else -> { // We wait for the lifecycle event ON_RESUME to be triggered before resetting the ViewModelStore // If in the mean time we are attached again, this work is cancelled attachedLifecycle .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() } // Wait for first event that matches ON_RESUME. .firstOrNull { it == Lifecycle.Event.ON_RESUME } ?: return@collectLatest resetViewModelStore() -
joost-klitsie created this gist
Aug 28, 2024 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,105 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.* import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @Composable fun rememberViewModelStoreOwner( key: Any?, ): ViewModelStoreOwner { val viewModelKey = "rememberViewModelStoreOwner#" + currentCompositeKeyHash.toString(36) val localLifecycle = LocalLifecycleOwner.current.lifecycle val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel>(key = viewModelKey) { ViewModelStoreOwnerViewModel(key, localLifecycle) } viewModelStoreOwnerViewModel.update(key, localLifecycle) DisposableEffect(Unit) { viewModelStoreOwnerViewModel.attachComposable() onDispose { viewModelStoreOwnerViewModel.detachComposable() } } return remember(viewModelStoreOwnerViewModel) { viewModelStoreOwnerViewModel.viewModelStore } } @Composable fun WithViewModelStoreOwner( key: Any?, content: @Composable () -> Unit, ) { CompositionLocalProvider( value = LocalViewModelStoreOwner provides rememberViewModelStoreOwner(key), content = content, ) } @Stable private class ViewModelStoreOwnerViewModel( private var key: Any?, initialLifecycle: Lifecycle, ) : ViewModel() { private val attachedLifecycle = MutableStateFlow(initialLifecycle) private val isAttachedToComposable = MutableSharedFlow<Boolean>() val viewModelStore: ViewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } init { viewModelScope.launch { isAttachedToComposable.collectLatest { isAttached -> when { // If we are attached or we are destroyed, we do not need to do anything isAttached || attachedLifecycle.value.currentState == Lifecycle.State.DESTROYED -> return@collectLatest // If we are detached and the lifecycle state is resumed, we should reset the view model store attachedLifecycle.value.currentState == Lifecycle.State.RESUMED -> resetViewModelStore() else -> { // We wait for the lifecycle event ON_RESUME to be triggered before resetting the ViewModelStore // If in the mean time we are attached again, this work is cancelled attachedLifecycle .flatMapLatest { lifecycle -> // Make sure we stop listening to the lifecycle events once we are destroyed lifecycle.eventFlow.takeWhile { it != Lifecycle.Event.ON_DESTROY } } // Wait for first event that matches ON_RESUME. .firstOrNull { it == Lifecycle.Event.ON_RESUME } ?: return@collectLatest resetViewModelStore() } } } } } fun update(key: Any?, lifecycle: Lifecycle) { if (key != this.key) { this.key = key resetViewModelStore() } attachedLifecycle.update { lifecycle } } fun attachComposable() { viewModelScope.launch { isAttachedToComposable.emit(true) } } fun detachComposable() { viewModelScope.launch { isAttachedToComposable.emit(false) } } override fun onCleared() { super.onCleared() resetViewModelStore() } private fun resetViewModelStore() { viewModelStore.viewModelStore.clear() } }