Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions chiptextfield-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ kotlin {
dependencies {
api(libs.jetbrains.compose.ui)
api(libs.jetbrains.compose.foundation)
implementation(libs.androidx.compose.runtime.retain)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ fun <T : Chip> BasicChipTextField(

LaunchedEffect(state, state.disposed) {
if (state.disposed) {
state.chips = state.defaultChips
if (state.resetChipsAfterLeavingComposition) {
state.chips = state.defaultChips
}
state.disposed = false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue

/**
Expand Down Expand Up @@ -35,6 +36,46 @@ fun <T : Chip> rememberChipTextFieldState(
return remember(key) { ChipTextFieldState(chips = chips) }
}

/**
* Return a [ChipTextFieldState] that is retained after leaving the composition.
*
* Retained state can survive configuration changes, but it does not survive process death. Use
* [rememberChipTextFieldState] when the state should only live as long as the current composition.
*
* @param chips Default chips
*/
@Composable
fun <T : Chip> retainChipTextFieldState(
chips: List<T> = emptyList(),
): ChipTextFieldState<T> {
return retain { retainedChipTextFieldState(chips) }
}

/**
* Return a [ChipTextFieldState] that is retained after leaving the composition, recreating it when
* [key] changes.
*
* Retained state can survive configuration changes, but it does not survive process death. Use
* [rememberChipTextFieldState] when the state should only live as long as the current composition.
*
* @param chips Default chips
* @param key An input that identifies the retained state. When this value changes, the previous
* state is discarded and a new state is created with [chips].
Comment on lines +54 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect the relevant section with line numbers.
sed -n '1,220p' chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt

Repository: dokar3/ChipTextField

Length of output: 6070


🌐 Web query:

androidx.compose.runtime.retain retain(key) key lifetime documentation short-lived reference Context View leak

💡 Result:

The retain API, introduced in androidx.compose.runtime:runtime-retain [1][2], is a mechanism for state memoization that provides a lifespan longer than remember but shorter than persistence solutions like rememberSaveable [1]. While remember is scoped to the immediate composition hierarchy, retained values are stored in a RetainedValuesStore and can persist through transient destruction and recreation (e.g., configuration changes) [3][1]. Crucially, retained values persist longer than the lifespan of the specific composable that created them [3]. This extended lifetime introduces significant risks of memory leaks if not managed correctly [3]. Memory Leak Prevention Guidelines: 1. Avoid Retaining Context and Views: Never retain an Android Context or any object that references a Context, either directly or indirectly [3]. This includes Views, as they hold references to the Context that created them [3]. A rule of thumb is that if you would not store an object in a ViewModel, you should not retain it using the retain API [1]. 2. Key Lifetime: The keys provided to retain are referenced as long as the value is retained [1]. Ensure that these keys themselves do not inadvertently hold onto long-lived references that could prevent garbage collection. 3. Use Annotations: To prevent accidental retention of unsafe types, you can annotate your class definition with androidx.compose.runtime.annotation.DoNotRetain [3]. 4. Resource Management: If your retained object manages resources, implement RetainObserver to receive callbacks like onRetained and onRetired [4][5]. Always release resources or cancel jobs in onRetired to prevent leaks [4][6]. 5. Explicit Disposal: When using utilities like RetainedValuesStoreRegistry, ensure that you call dispose when the registry is no longer needed [6]. Failure to do so may result in leaked memory from undispatched RetainObserver callbacks [6]. In summary, retain is a powerful tool for maintaining state across configuration changes without serialization, but it effectively acts as a cache that lives as long as the app process (or the associated RetainedValuesStore). Treat it with the same memory-safety caution as you would a ViewModel [1].

Citations:


Document the retained key lifetime constraint.
retainChipTextFieldState(..., key) keeps the key alive as long as the retained value, so avoid passing short-lived references like Context or View-backed objects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt`
around lines 54 - 63, Update the KDoc for retainChipTextFieldState, specifically
the key parameter documentation, to state that the key remains alive for the
lifetime of the retained value. Warn callers not to pass short-lived references
such as Context or View-backed objects.

*/
@Composable
fun <T : Chip> retainChipTextFieldState(
chips: List<T> = emptyList(),
key: Any?,
): ChipTextFieldState<T> {
return retain(key) { retainedChipTextFieldState(chips) }
}

private fun <T : Chip> retainedChipTextFieldState(chips: List<T>): ChipTextFieldState<T> {
return ChipTextFieldState(chips = chips).apply {
resetChipsAfterLeavingComposition = false
}
}

/**
* State class for [BasicChipTextField]
*
Expand All @@ -44,6 +85,8 @@ fun <T : Chip> rememberChipTextFieldState(
class ChipTextFieldState<T : Chip>(
chips: List<T> = emptyList()
) {
internal var resetChipsAfterLeavingComposition = true

internal var disposed = false

internal var defaultChips: List<T> = chips
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ androidMinSdk = "23"
kotlin = "2.4.10"
coil = "3.5.0"
compose-multiplatform = "1.11.1"
androidx-compose-runtime = "1.11.2"
coroutines = "1.11.0"
agp = "9.3.0"
maven-publish = "0.37.0"
Expand All @@ -29,6 +30,7 @@ coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "
ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" }
androidx-compose-runtime-retain = { module = "androidx.compose.runtime:runtime-retain", version.ref = "androidx-compose-runtime" }
jetbrains-compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" }
jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" }
jetbrains-compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "compose-multiplatform" }
Expand Down
Loading