Skip to content

Instantly share code, notes, and snippets.

@manuelvicnt
Last active March 23, 2024 17:22
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save manuelvicnt/a2e4c4812243ac1b218b24d0ac8d22bb to your computer and use it in GitHub Desktop.
Save manuelvicnt/a2e4c4812243ac1b218b24d0ac8d22bb to your computer and use it in GitHub Desktop.
Scope ViewModels to Composables
// PLEASE, READ
//
// This is a way to scope ViewModels to the Composition.
// However, this doesn't survive configuration changes or procress death on its own.
// You can handle all config changes in compose by making the activity handle those in the Manifest file
// e.g. android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode">
//
// This is just an exploration to see what's possible in Compose. We don't encourage developers to copy-paste
// this code if they don't fully understand the implications of it and if this actually solves the use case to solve.
/*
* Composition-aware ViewModelStoreOwner
*/
internal class CompositionScopedViewModelStoreOwner : ViewModelStoreOwner, RememberObserver {
private val viewModelStore = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = viewModelStore
override fun onAbandoned() {
viewModelStore.clear()
}
override fun onForgotten() {
viewModelStore.clear()
}
override fun onRemembered() {
// Nothing to do here
}
}
/*
* Applies a [CompositionScopedViewModelStore] value to [LocalViewModelStoreOwner]
* to be able to scope ViewModels to a certain subtree of the Composition.
*
* Note: It's not a good idea to use `ProvideViewModels` at a screen level due to
* https://twitter.com/ianhlake/status/1395128325811494913. As when the store leaves
* the Composition, all state and ViewModels are lost.
* It's a good idea to use `ProvideViewModels` whenever it makes sense, for example,
* before starting a certain flow of your app that have multiple screens with their own VM.
* For example, a login flow, registration flow, or the main app flow.
*/
@Composable
fun ProvideViewModels(content: @Composable () -> Unit) {
val viewModelStoreOwner = remember { CompositionScopedViewModelStoreOwner() }
CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) {
content()
}
}
/*
Example of how you'd use this:
@Composable
fun MyApp() {
// ...
// Scope ViewModels in this part of the Composition.
ProvideViewModels {
val viewModel: SharedLoginViewModel = viewModel(factory = getSharedLoginViewModelFactory())
LoginScreensFlow(viewModel)
}
}
@Composable
fun LoginScreensFlow(viewModel: SharedLoginViewModel) { /* ... */ }
*/
@OKatrych
Copy link

Here is my solution to make it survive the configuration changes:

@Composable
internal fun ProvidesViewModelStoreOwner(
    ownerKey: String = rememberSaveable { UUID.randomUUID().toString() },
    content: @Composable () -> Unit,
) {
    val context = LocalContext.current

    remember {
        object : RememberObserver {
            override fun onRemembered() = Unit
            override fun onAbandoned() {
                clear()
            }

            override fun onForgotten() {
                clear()
            }

            private fun clear() {
                val isChangingConfigurations = context.findActivity().isChangingConfigurations
                if (!isChangingConfigurations) {
                    AppVMStoreOwnersHolder.remove(ownerKey)
                }
            }
        }
    }

    val viewModelStoreOwner: ViewModelStoreOwner = remember(ownerKey) {
        AppVMStoreOwnersHolder.getOwner(ownerKey)
    }

    CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) {
        content()
    }
}

internal object AppVMStoreOwnersHolder {
    private val lock = ReentrantLock()
    private val storeOwnerMap: MutableMap<String, ViewModelStoreOwner> = mutableMapOf()

    fun getOwner(key: String): ViewModelStoreOwner = lock.withLock {
        storeOwnerMap.getOrPut(key) {
            object : ViewModelStoreOwner {
                override val viewModelStore = ViewModelStore()
            }
        }
    }

    fun remove(key: String) = lock.withLock {
        Timber.d("Remove $key")
        storeOwnerMap[key]?.viewModelStore?.clear()
        storeOwnerMap.remove(key)
    }
}

/*
Example of how you'd use this:
@Composable
fun MyApp() {
    // ...
    
    ProvidesViewModelStoreOwner {
        LoginScreensFlow(viewModel)
    }
}
@Composable
fun LoginScreensFlow(viewModel: LoginViewModel = koinViewModel()) { /* ... */ }
*/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment