compose-cache is a lightweight utility for Jetpack Compose that helps you handle two-way state synchronization between UI and external sources (e.g. databases, flows) — without suffering from cursor jumps or overwritten user input.
Note
🚀 RevealSwipe is now Compose Multiplatform
In Compose, text fields are typically bound to a single source of truth — for example, a StateFlow or immutable UI state provided by a ViewModel.
When the user types, you usually:
- Send the new input to the ViewModel (
onValueChange) - The ViewModel emits a new UI state (often via
combine,copy, or other transformations) - The UI collects this state and updates the
TextFieldvalue accordingly
The issue arises because state emission and recomposition are asynchronous. If the user types quickly, there can be a short delay before the ViewModel emits the updated state. During that window:
- The
TextFieldstill receives the old value from the last emission - Compose updates the UI with that outdated value
- This causes the cursor to jump or recently typed characters to disappear
This can happen even without databases — simply using collectAsState() with a ViewModel is enough to reproduce it, especially in multi-field forms or with combined UI state.
rememberForUserInput wraps your state with a temporary local cache.
When the user types, it stores the input locally and marks the state as dirty.
While dirty, external updates are ignored to prevent overwriting user input.
Once the external source emits the same value that the user typed, the dirty flag is cleared and control is handed back to the source.
👉 This means the UI stays responsive, and your database stays the single source of truth — without flicker or cursor issues.
Add actual compose-cache library:
dependencies {
implementation 'de.charlex.compose:compose-cache:3.0.1'
}rememberForUserInput can be used for every value/onValueChange behaviors where race conditions occurs
val (text, onTextChange) = rememberForUserInput(
value = dbValue,
onValueChange = { newValue ->
viewModel.updateDatabase(newValue)
}
)
TextField(
value = text,
onValueChange = onTextChange
)If your value isn’t a simple primitive, you can provide a custom equals function:
val (item, onItemChange) = rememberForUserInput(
value = selectedItem,
equals = { a, b -> a.id == b.id },
onValueChange = { newItem -> updateInDb(newItem) }
)Copyright 2022 Alexander Karkossa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.