A RetainScopeHolder creates and manages RetainScope instances for collections of items. This is desirable for components that swap in and out children where each child should be able to retain state when it becomes removed from the composition hierarchy.

To use this class, call getOrCreateRetainScopeForChild to instantiate the RetainScope that should be installed for a given child content block. For automatic installation and content tracking, wrap your content in RetainScopeProvider.

You can also install the managed retain scopes manually by obtaining a RetainScope with getOrCreateRetainScopeForChild and setting it as the LocalRetainScope for your children's content. When a child is being removed, call startKeepingExitedValues to begin the transient destruction phase of your retention scenario. After the child has been added back to the composition, invoke stopKeepingExitedValues to finalize the restoration of retained values.

When a RetainScopeHolder is no longer used, you must call dispose before the provider is garbage collected. This ensures that all retained values are correctly retired. Failure to do so may result in leaked memory from undispatched RetainObserver.onRetired callbacks. Instances created by retainRetainScopeHolder are automatically disposed when the provider stops being retained.

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.retain.retainRetainScopeHolder
import androidx.compose.ui.graphics.painter.Painter

// List item that retains a value
@Composable
fun Contact(contact: Contact) {
    Row {
        // Retain this painter to cache the contact icon in memory
        val contactIcon = retain { ContactIconPainter() }
        Image(contactIcon, "Contact icon")
        Text(contact.name)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    // Create the RetainScopeHolder
    val retainScopeHolder = retainRetainScopeHolder()
    LazyColumn {
        items(contacts) { contact ->
            // Install it for an item in a list
            retainScopeHolder.RetainScopeProvider(contact.id) {
                // This contact now gets its own retain scope.
                // If the scope of ContactsList starts keeping exited values, this nested
                // scope will too. If this contact leaves re-enters composition, it will keep
                // its previously retained values.
                Contact(contact)
            }
        }
    }

    // Optional: Purge child scopes if a contact gets deleted.
    DisposableEffect(contacts) {
        val contactIdsSet = contacts.map { it.id }
        retainScopeHolder.clearChildren { it !in contactIdsSet }
        onDispose {}
    }
}

Summary

Public constructors

Cmn

Public functions

Unit
@Composable
RetainScopeProvider(key: Any?, content: @Composable () -> Unit)

Installs child content that should be retained under the given key.

Cmn
Unit
clearChild(key: Any?)

Removes the RetainScope for the child with the given key from this RetainScopeHolder.

Cmn
Unit
clearChildren(predicate: (Any?) -> Boolean)

Bulk removes all child scopes for which the predicate returns true.

Cmn
Unit

Removes all child RetainScopes from this RetainScopeHolder and marks it as ineligible for future use.

Cmn
RetainScope

Creates or returns a previously created RetainScope instance for the given key.

Cmn
Int

Gets the total number of active requests from startKeepingExitedValues for the given key.

Cmn
Unit

When a RetainStateProvider is set as the parent of a RetainScopeHolder, the RetainScopeHolder will mirror the retention state of the parent.

Cmn
Unit

Starts keeping exited values for a child with the given key.

Cmn
Unit

Stops keeping exited values for a child with the given key as previously started by startKeepingExitedValues.

Cmn

Public constructors

RetainScopeHolder

RetainScopeHolder()

Public functions

RetainScopeProvider

@Composable
fun RetainScopeProvider(key: Any?, content: @Composable () -> Unit): Unit

Installs child content that should be retained under the given key. startKeepingExitedValues and stopKeepingExitedValues and automatically called based on the presence of this composable for the key.

When removed, this composable begins keeping exited values from the content lambda under the given key. When added back to the composition hierarchy, the scope will stop keeping retained values once the composition completes. The keys used with this method should only be used once per RetainScopeHolderin a composition.

This composable only attempts to manage the retention lifecycle for the content and key pair. It will retain removed content indefinitely until clearChild or clearChildren is invoked.

Parameters
key: Any?

The child key associated with the given content. This key is used to identify the retention pool for objects retained by the content composable.

content: @Composable () -> Unit

The composable content to compose with the RetainScope of the given key

Throws
kotlin.IllegalStateException

if dispose has been called

clearChild

fun clearChild(key: Any?): Unit

Removes the RetainScope for the child with the given key from this RetainScopeHolder. If the key doesn't have an associated RetainScope yet (either because it hasn't been created or has already been cleared), this function does nothing.

If the scope being cleared is currently keeping exited values, it will stop as a result of this call. If a child with the given key is currently in the composition hierarchy, its retained values will not be persisted the next time the child content is destroyed. Orphaned RetainScopes will never begin keeping exited values, and the content will need to be recreated with a new RetainScope before exited values will be kept again.

If getOrCreateRetainScopeForChild is called again for the given key, a new RetainScope will be created and returned.

Parameters
key: Any?

The key of the child content whose RetainScope should be discarded

clearChildren

fun clearChildren(predicate: (Any?) -> Boolean): Unit

Bulk removes all child scopes for which the predicate returns true. This function follows the same clearing rules as clearChild.

Parameters
predicate: (Any?) -> Boolean

The predicate to evaluate on all child keys in the RetainScopeHolder. If the predicate returns true for a given key, it will be cleared. If the predicate returns false it will remain in the collection.

See also
clearChild

dispose

fun dispose(): Unit

Removes all child RetainScopes from this RetainScopeHolder and marks it as ineligible for future use. This is required to invoke when the scope is no longer used to retire any retained values. Failing to do so may result in memory leaks from undispatched RetainObserver.onRetired and RetainedEffect callbacks. When this function is called, all values retained in scopes managed by this provider will be immediately retired.

If this scope has already been disposed, this function will do nothing.

getOrCreateRetainScopeForChild

fun getOrCreateRetainScopeForChild(key: Any?): RetainScope

Creates or returns a previously created RetainScope instance for the given key. The returned RetainScope will be managed by this provider. It will begin retaining if the parent retain scope starts retaining or if startKeepingExitedValues is called with the same key, and it will stop retaining with the parent retain scope ends retaining and there is no startKeepingExitedValues call without a corresponding stopKeepingExitedValues call for the specified key.

The first time this function is called for a given key, a new RetainScope is created for the key. When this function is called for the same key, it will return the same RetainScope it originally returned. If a given key's scope is cleared, then a new one will be created for it the next time it is requested via this function.

This function must be called before startKeepingExitedValues or stopKeepingExitedValues is called for those two methods to have any effect on the retention state for the given key.

Parameters
key: Any?

The key to return an existing RetainScope instance for, if one exists, or to create a new instance for

Returns
RetainScope

A RetainScope instance suitable to be installed as the LocalRetainScope for the child content with the specified key

Throws
kotlin.IllegalStateException

if dispose has been called

keepExitedValuesRequestsFor

fun keepExitedValuesRequestsFor(key: Any?): Int

Gets the total number of active requests from startKeepingExitedValues for the given key. Effectively, this is the number of calls to startKeepingExitedValues minus the number of calls to stopKeepingExitedValues for the given key.

This counter resets if clearScope is called for the given key. If the scope has not been created for key by getOrCreateRetainScopeForChild, this function will return 0.

Parameters
key: Any?

the key of the child to look up

Returns
Int

The number of active requests against the given child to keep exited values

setParentRetainStateProvider

fun setParentRetainStateProvider(parent: RetainStateProvider): Unit

When a RetainStateProvider is set as the parent of a RetainScopeHolder, the RetainScopeHolder will mirror the retention state of the parent. If the parent stops retaining, all children that have started retaining via startKeepingExitedValues will continue being retained after the parent stops retaining.

If this function is called twice, the new parent will replace the old parent. The new parent's state is immediately applied to the child scopes.

To clear a parent, call this function and pass in either RetainStateProvider.AlwaysKeepExitedValues or RetainStateProvider.NeverKeepExitedValues depending on whether you want this scope to keep exited values in the absence of a parent.

startKeepingExitedValues

fun startKeepingExitedValues(key: Any?): Unit

Starts keeping exited values for a child with the given key. If a retain scope has not been created for this key (because getOrCreateRetainScopeForChild was not called for the key or it has been cleared with clearChild or clearChildren), then this function does nothing. If the retain scope for the given key is already keeping exited values, the scope will not change states. The number of times this function is called is tracked and must be matched by the same number of calls to stopKeepingExitedValues for the given key before its kept values will be retired.

This function must be called before any content for the associated child is removed from the composition hierarchy.

Parameters
key: Any?

The key of the child to begin retention for

stopKeepingExitedValues

fun stopKeepingExitedValues(key: Any?): Unit

Stops keeping exited values for a child with the given key as previously started by startKeepingExitedValues. If the underlying scope is not retaining because startKeepingExitedValues has not been called, this function will throw an exception. If no such retain scope exists because it was cleared with clearChild or never created with getOrCreateRetainScopeForChild, this function will do nothing.

If startKeepingExitedValues has been called more than stopKeepingExitedValues, the scope will continue to keep retained values that have exited the composition until stopKeepingExitedValues has been called the same number of times as startKeepingExitedValues.

This function must be called after the completion of the frame in which the child content is being restored to allow the restored child to re-consume all of its retained values. You can use androidx.compose.runtime.Recomposer.scheduleFrameEndCallback or androidx.compose.runtime.Composer.scheduleFrameEndCallback to insert a sufficient delay.

Parameters
key: Any?

The key of the child to end retention for

Throws
kotlin.IllegalStateException

if startKeepingExitedValues is called more times than stopKeepingExitedValues has been called for the given key