Abhimanyu
09/02/2025, 2:32 PMTextFieldState
with the subsequent values emitted from the viewmodel.
val usernameState = rememberTextFieldState(initialUiState.username)
LaunchedEffect(usernameState) {
snapshotFlow { usernameState.text.toString() }.collectLatest {
loginViewModel.updateUsername(it)
}
}
Could anyone please help share how to solve this?Eddie
09/02/2025, 3:00 PMAbhimanyu
09/02/2025, 3:02 PMTextFieldState
is not updated once the data is fetched as it is remembered.Eddie
09/02/2025, 3:03 PMrememberTextFieldState
inside an if
block so that neither gets rendered until the data is fetched.Winson Chiu
09/02/2025, 3:14 PMAbhimanyu
09/02/2025, 3:17 PMString
instead of TextFieldState
.
I am following the same approach as my current architecture has a single StateFlow from the ViewModel to the UI layer.Winson Chiu
09/02/2025, 3:21 PMYour ViewModel will still have the latest values from UI, but its uiState: StateFlow<UiState> won't be driving the TextFields.
Abhimanyu
09/02/2025, 3:21 PMTextFieldState
.Eddie
09/02/2025, 3:22 PMAbhimanyu
09/02/2025, 3:23 PMTextFieldState
kodee sad .Abhimanyu
09/02/2025, 3:25 PMTextFieldState
based on some events with this 😕 ?Winson Chiu
09/02/2025, 3:27 PMAbhimanyu
09/02/2025, 3:28 PMYou could also use some key to invalidate the remembered state and re-initialize it, but that feels very complicated.Already working on this, not able to make it work completely.
Abhimanyu
09/02/2025, 3:30 PMTextFieldState
break UDF 🤔 ?
https://developer.android.com/develop/ui/compose/architecture
TextField
component is used as the example here to showcase UDF.
What is the reasoning behind storing TextFieldState
in the ViewModel if it is a two way communication data structure?Abhimanyu
09/02/2025, 3:55 PMWinson Chiu
09/02/2025, 4:04 PMAbhimanyu
09/02/2025, 4:06 PMAbhimanyu
09/02/2025, 4:07 PMWinson Chiu
09/02/2025, 4:08 PMAbhimanyu
09/02/2025, 4:13 PMprivate val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
val uiState: State<UiState>
get() = _uiState
If the uiState is Mutable State, composable can directly update it from the UI.
In case of TextFieldState
, it seems similar to mutable state rather than immutable state, to enable two way data communication.Abhimanyu
09/02/2025, 4:16 PMThe UI layer should never change state outside of an event handler because this can introduce inconsistencies and bugs in your application.
Prefer passing immutable values for state and event handler lambdas.
• You ensure that your UI doesn't change the value of the state directly.
Winson Chiu
09/02/2025, 4:16 PMfun setState
exposed in the VM which Compose calls anyways, then making the state itself mutable is just an abstraction of that update.Abhimanyu
09/02/2025, 4:38 PMCan you change your UiState to just hold the state instead?This is working for the basic use-cases I have checked so far. This also requires using
snapshotFlow
to collect the latest text as the text update now no longer is via a setter method.
coroutineScope.launch {
snapshotFlow { title.text.toString() }.collectLatest {
// Use the text for validation and other use-cases.
}
}
Zach Klippenstein (he/him) [MOD]
09/02/2025, 5:02 PMTextFieldState
is actually more compatible with UDF since it lets you fully own the source of truth for what’s in the field.
If you want a callback for when text changes for things like validation, see InputTransformation
.Abhimanyu
09/03/2025, 2:47 AMInputTransformation
intended to be used as a replacement for onValueChange
for validation?
The use case is to check if the screen CTA can be enabled based on the value entered in the Text Field.
There’s no input transformation in this scenario, so IMHO, using InputTransformation would be confusing.Winson Chiu
09/03/2025, 2:54 AMderivedStateOf
on there if you want to avoid recomposing the button.Abhimanyu
09/03/2025, 2:58 AMIs the calculation expensive?To some extent yes, it may involve database data fetching, datastore data fetching or file reading. (Some IO)
Zach Klippenstein (he/him) [MOD]
09/03/2025, 4:20 AMAbhimanyu
09/03/2025, 4:42 AMScreenUIState
@Stable
internal data class ScreenUIState(
val titleTextFieldState: TextFieldState = TextFieldState(),
// Other data
)
ViewModel
private val _uiState: MutableStateFlow<ScreenUIState> = MutableStateFlow(ScreenUIState())
internal val uiState: StateFlow<ScreenUIState> = _uiState.asStateFlow()
fun initViewModel() {
coroutineScope.launch { snapshotFlow { title.text.toString() }.collectLatest {
// Validate and update ui state
}}
}
It seems to work for the use-cases I have tested so far.
Please do share if this can be improved.
I would ideally like to:
1. emit a single UI State data class from the ViewModel.
2. And if possible, store the text field data as only string in the ViewModel. (Not able to do this so far)dorche
09/03/2025, 2:36 PMWinson Chiu
09/03/2025, 2:47 PMZach Klippenstein (he/him) [MOD]
09/03/2025, 3:31 PMDoesn't putting the TextFieldState inside a StateFlow reintroduce one of the original issues BTF2 was trying to solve - lag between user input and screen updates?It should be the same instance of TFS in every state (in most cases), so no.
Abhimanyu
09/06/2025, 10:09 AMTextFieldState
to the UI State data class and emit it using StateFlow is that the StateFlow does not emit an event when only the TextFieldState
text content changes (expected as the instance of the TextFieldState
remains the same).
This works for the production code. But, testing is now a bit more confusing.
@Test
fun clearTitle_shouldClearText() = runTest {
val updatedTitle = "testTitle"
screenViewModel.uiState.test {
awaitItem().isLoading.shouldBeFalse()
screenViewModel.updateTitle(updatedTitle)
val result = awaitItem()
result.titleTextFieldState.text.toString().shouldBe(updatedTitle)
screenViewModel.clearTitle()
result.titleTextFieldState.text.toString().shouldBeEmpty() // Can not use awaitItem() here as there is no new emission in the StateFlow, we have to use the previous emitted item
}
}
Halil Ozercan
09/15/2025, 10:12 AMonValueChange
would update the value that’s held in this StateFlow.
However, the problems start when we take a closer look at this tight seeming loop (value, onValueChange). Sidenote; I can talk about the async nature of StateFlow and how it causes certain problems (https://medium.com/androiddevelopers/effective-state-management-for-textfield-in-compose-d6e5b070fbe5) here but let’s skip that.
The fundamental handicap of callback based state keeping in Compose is that compose runs in phases. If a composable hoists its main state, as we used to in TextField(value)
, then Composition is your only timing to instruct the Composable to update its state and sync with the value coming from your ViewModel or anywhere else. In other words, if you would like to filter the input coming from the user, there is a gap between the user's input and the next composition cycle where things stand a little vague. An example, you want to have a numeric filter for your phone number field, so you want to prevent people from typing letters through their hardware keyboard;
• User types a
• onValueChange
triggers with a
included
• Currently the internal state of TextField has a
in it.
• Your ViewModel removes the a
, passes along the filtered value
• TextField updates its internal state
• Currently the internal state of TextField has no a
in it.
Now think about what happens if there is concurrency involved (StateFlow) and the UI update lacks one frame behind.
Well this shouldn’t be an all new problem. Many UI components work the same way and have the same interception requirements. What about a checkbox? The same problem can definitely be said about a checkbox too, what if we stop its state progression from checked to unchecked and concurrency messes up the frame timing?
In that case, checkbox has the peace of mind that it doesn’t report its state to multiple clients. When the user clicks on it;
• Checkbox reports a change from unchecked to checked
• Checkbox doesn’t update its internal state. It is awaiting the hoisted state to change
• Your state holder either accepts the change, or blocks it
• Next composition cycle decides what to render.
Alright, then why does TextField update its internal state immediately before awaiting the state holder to decide what should happen? The answer is IME. InputMethodEditor or also known as Software Keyboard is always in communication with the editor/TextField. This communication happens very sequentially and does not respect concurrency at all. IME tells something to the Editor and expects its result immediately, so that it can manage its own state predictably. All in all, IME is a separate process and it can only communicate with your editor through messaging (ask -> receive answer). Let’s take a look at the filtering problem with now IME
in mind.
• User types a
• IME sends a command UserTyped(a)
• TextField updates its internal state with a
.
◦ TextField at this point cannot ask the user about the next value. That will happen during the next composition cycle.
• IME asks back what happened? TextField replies the current state has a
in it.
• onValueChange
triggers with a
included.
• Your ViewModel removes the a
, passes along the filtered value
.
• TextField updates its internal state.
• IME should be notified that the editor state has changed outside of its knowledge.
◦ ResetIME
is called.
• At this point IME may flicker, may lose its composing range, and any other side effect that comes with ResetIME
call.
The crucial part here is that IME
expects an answer immediately from the TextField at any time.
I can write even more and maybe this should be a blog post of its own but basically the (value, onValueChange)
design is not a sustainable approach to a text editor state in Compose. We needed a more tightly closed loop like the one that is managed internally by TextFieldState
. I realize that it sways away from the singular UiState approach but it definitely doesn’t break UDF.
The good news is that you can still opt-in to value, onValueChange
(https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt;l=39) if you know what you are doing and extensively tested your application with many different types of Software Keyboards (gboard, swiftkey, samsung, etc.).
Ps; I simply wanted to highlight one of the many challenges behind designing a TextField API. There are even more scenarios where value, onValueChange
breaks the expected behavior like granular editing.Halil Ozercan
09/15/2025, 10:33 AMInputTransformation
. If that looks infeasible, please share your case so we can take a look at it together.Abhimanyu
09/15/2025, 11:02 AMTextFieldState
from the existing callback-based component?
If yes, what’s the expectation for apps using an architecture that emits a single StateFlow from the ViewModel?
Will there be better support for this, or will that architecture not work well with TextFieldState
?
https://developer.android.com/develop/ui/compose/text/migrate-state-based#viewmodel-stateflowHalil Ozercan
09/15/2025, 11:09 AMTextFieldState
and eventually deprecating value, onValueChange
• I believe the expectation is explained in the migration guide that you linked. I'm afraid there won't be a single magical solution since the problem we are trying to solve has many edges. However we will try to improve the guide with more examples and advice, encompassing all the different use cases that are brough up by the developers.
• Again I believe that architecture can work with the new TextFieldState
but there are multiple ways to make them work together. It is going to depend on your specific setup and how you are using TextField.Abhimanyu
09/15/2025, 11:19 AMScreenUiState
data class with required data. The initial state of TextFieldState
would be an empty string.
• Once data is fetched (from database, network, file, shared preference, data store, etc.) ⇒ emit an updated ScreenUiState
with the new state of TextFieldState
.
The issue is that the second UI state emission isn’t triggered if the only difference is the text in TextFieldState
. This is expected since StateFlow emits only when the underlying object instance changes.
Could you please share if there are any recommendations for this test case?Halil Ozercan
09/15/2025, 1:53 PMTextFieldState
using TextFieldState.edit { }
and your TextField should update automatically. For testing, you can simply check the TextFieldState.text
value to verify that text change happened as expected.Halil Ozercan
09/15/2025, 1:54 PMTextFieldState.text
change using a snapshotFlow
. We are preparing more documentation to cover the testing strategies for TextFieldState
and in general the new TextFieldWinson Chiu
09/15/2025, 5:14 PMyou don't need to emit a new state
I think the idea is that if you're going to follow a single UiState data class, you want the emissions of the stream to exactly mirror the updates to the UI. I'm exploring the different mechanisms to model state in our internal recommended architecture, and it seems hard to justify the single UiState model when you violate that principle. It also seems overly restrictive when just having multiple sibling state fields is easier to manage. No massive combine mechanism to get to that single UiState.