Migrate to state-based text fields
Stay organized with collections
Save and categorize content based on your preferences.
This page provides examples of how you can migrate value-based TextFields to
state-based TextFields. See the Configure text fields page for
information on the differences between value and state-based TextFields.
Basic usage
Value-based
@Composable
funOldSimpleTextField(){
varstatebyrememberSaveable{mutableStateOf("")}
TextField(
value=state,
onValueChange={state=it},
singleLine=true,
)
}
State-based
@Composable
funNewSimpleTextField(){
TextField(
state=rememberTextFieldState(),
lineLimits=TextFieldLineLimits.SingleLine
)
}
- Replace the
value, onValueChange, andremember { mutableStateOf("")} withrememberTextFieldState(). - Replace
singleLine = truewithlineLimits = TextFieldLineLimits.SingleLine.
Filtering through onValueChange
Value-based
@Composable
funOldNoLeadingZeroes(){
varinputbyrememberSaveable{mutableStateOf("")}
TextField(
value=input,
onValueChange={newText->
input=newText.trimStart{it=='0'}
}
)
}
State-based
@Preview
@Composable
funNewNoLeadingZeros(){
TextField(
state=rememberTextFieldState(),
inputTransformation=InputTransformation{
while(length > 0 && charAt(0)=='0')delete(0,1)
}
)
}
- Replace the value callback loop with
rememberTextFieldState(). - Re-implement the filtering logic in
onValueChangeusingInputTransformation. - Use
TextFieldBufferfrom the receiver scope ofInputTransformationto update thestate.InputTransformationis called exactly right after user input is detected.- Changes that are proposed by
InputTransformationthroughTextFieldBufferare applied immediately, avoiding a synchronization issue between the software keyboard andTextField.
Credit card formatter TextField
Value-based
@Composable
funOldTextFieldCreditCardFormatter(){
varstatebyremember{mutableStateOf("")}
TextField(
value=state,
onValueChange={if(it.length<=16)state=it},
visualTransformation=VisualTransformation{text->
// Making XXXX-XXXX-XXXX-XXXX string.
varout=""
for(iintext.indices){
out+=text[i]
if(i%4==3 && i!=15)out+="-"
}
TransformedText(
text=AnnotatedString(out),
offsetMapping=object:OffsetMapping{
overridefunoriginalToTransformed(offset:Int):Int{
if(offset<=3)returnoffset
if(offset<=7)returnoffset+1
if(offset<=11)returnoffset+2
if(offset<=16)returnoffset+3
return19
}
overridefuntransformedToOriginal(offset:Int):Int{
if(offset<=4)returnoffset
if(offset<=9)returnoffset-1
if(offset<=14)returnoffset-2
if(offset<=19)returnoffset-3
return16
}
}
)
}
)
}
State-based
@Composable
funNewTextFieldCreditCardFormatter(){
valstate=rememberTextFieldState()
TextField(
state=state,
inputTransformation=InputTransformation.maxLength(16),
outputTransformation=OutputTransformation{
if(length > 4)insert(4,"-")
if(length > 9)insert(9,"-")
if(length > 14)insert(14,"-")
},
)
}
- Replace the filtering in
onValueChangewith anInputTransformationto set the max length of the input.- Refer to the Filtering through
onValueChangesection.
- Refer to the Filtering through
- Replace
VisualTransformationwithOutputTransformationto add in dashes.- With
VisualTransformation, you are responsible for both creating the new text with the dashes and also calculating how the indices are mapped between the visual text and the backing state. OutputTransformationtakes care of the offset mapping automatically. You just need to add the dashes in correct places using theTextFieldBufferfromOutputTransformation.transformOutput's receiver scope.
- With
Updating the state (simple)
Value-based
@Composable
funOldTextFieldStateUpdate(userRepository:UserRepository){
varusernamebyremember{mutableStateOf("")}
LaunchedEffect(Unit){
username=userRepository.fetchUsername()
}
TextField(
value=username,
onValueChange={username=it}
)
}
State-based
@Composable
funNewTextFieldStateUpdate(userRepository:UserRepository){
valusernameState=rememberTextFieldState()
LaunchedEffect(Unit){
usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername())
}
TextField(state=usernameState)
}
- Replace the value callback loop with
rememberTextFieldState(). - Change the value assignment with
TextFieldState.setTextAndPlaceCursorAtEnd.
Updating the state (complex)
Value-based
@Composable
funOldTextFieldAddMarkdownEmphasis(){
varmarkdownStatebyremember{mutableStateOf(TextFieldValue())}
Button(onClick={
// add ** decorations around the current selection, also preserve the selection
markdownState=with(markdownState){
copy(
text=buildString{
append(text.take(selection.min))
append("**")
append(text.substring(selection))
append("**")
append(text.drop(selection.max))
},
selection=TextRange(selection.min+2,selection.max+2)
)
}
}){
Text("Bold")
}
TextField(
value=markdownState,
onValueChange={markdownState=it},
maxLines=10
)
}
State-based
@Composable
funNewTextFieldAddMarkdownEmphasis(){
valmarkdownState=rememberTextFieldState()
LaunchedEffect(Unit){
// add ** decorations around the current selection
markdownState.edit{
insert(originalSelection.max,"**")
insert(originalSelection.min,"**")
selection=TextRange(originalSelection.min+2,originalSelection.max+2)
}
}
TextField(
state=markdownState,
lineLimits=TextFieldLineLimits.MultiLine(1,10)
)
}
In this use case, a button adds the Markdown decorations to make the text bold around the cursor or the current selection. It also maintains the selection position after the changes.
- Replace the value callback loop with
rememberTextFieldState(). - Replace
maxLines = 10withlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Change the logic of calculating a new
TextFieldValuewith aTextFieldState.editcall.- A new
TextFieldValueis generated by splicing the existing text based on the current selection, and inserting the Markdown decorations in between. - Also the selection is adjusted according to new indices of the text.
TextFieldState.edithas a more natural way of editing the current state with the use ofTextFieldBuffer.- The selection explicitly defines where to insert the decorations.
- Then, adjust the selection, similar to the
onValueChangeapproach.
- A new
ViewModel StateFlow architecture
Many applications follow the Modern app development guidelines, which
promote using a StateFlow to define the UI state of a screen or a component
through a single immutable class that carries all the information.
In these types of applications, a form like a Login screen with text input is usually designed as follows:
classLoginViewModel:ViewModel(){
privateval_uiState=MutableStateFlow(UiState())
valuiState:StateFlow<UiState>
get()=_uiState.asStateFlow()
funupdateUsername(username:String)=_uiState.update{it.copy(username=username)}
funupdatePassword(password:String)=_uiState.update{it.copy(password=password)}
}
dataclassUiState(
valusername:String="",
valpassword:String=""
)
@Composable
funLoginForm(
loginViewModel:LoginViewModel,
modifier:Modifier=Modifier
){
valuiStatebyloginViewModel.uiState.collectAsStateWithLifecycle()
Column(modifier){
TextField(
value=uiState.username,
onValueChange={loginViewModel.updateUsername(it)}
)
TextField(
value=uiState.password,
onValueChange={loginViewModel.updatePassword(it)},
visualTransformation=PasswordVisualTransformation()
)
}
}
This design perfectly fits with the TextFields that use the value,
onValueChange state hoisting paradigm. However, there are unpredictable
downsides to this approach when it comes to text input. The deep synchronization
issues with this approach are explained in detail in the Effective state
management for TextField in Compose blog post.
The problem is that the new TextFieldState design is not directly compatible
with the StateFlow backed ViewModel UI state. It may look strange to replace
username: String and password: String with username: TextFieldState and
password: TextFieldState, since TextFieldState is an inherently mutable data
structure.
A common recommendation is to avoid placing UI dependencies into a ViewModel.
Although this is generally a good practice, it can sometimes be misinterpreted.
This is particularly true for Compose dependencies that are purely data
structures and don't carry any UI elements with them, like TextFieldState.
Classes like MutableState or TextFieldState are simple state holders that
are backed by Compose's Snapshot state system. They are no different from
dependencies like StateFlow or RxJava. Therefore,we encourage you to
re-evaluate how you apply the "no UI dependencies in ViewModel" principle in
your code. Keeping a reference to a TextFieldState within your ViewModel is
not an inherently bad practice.
Recommended simple approach
We recommend you extract values like username or password from UiState,
and keep a separate reference for them in the ViewModel.
classLoginViewModel:ViewModel(){
valusernameState=TextFieldState()
valpasswordState=TextFieldState()
}
@Composable
funLoginForm(
loginViewModel:LoginViewModel,
modifier:Modifier=Modifier
){
Column(modifier){
TextField(state=loginViewModel.usernameState,)
SecureTextField(state=loginViewModel.passwordState)
}
}
- Replace
MutableStateFlow<UiState>with a coupleTextFieldStatevalues. - Pass those
TextFieldStateobjects toTextFieldsin theLoginFormcomposable.
Conforming approach
These types of architectural changes are not always easy. You may not have the
freedom to make these changes, or the time investment could outweigh the
benefits of using the new TextFields. In this case, you can still use
state-based text fields with a little tweak.
classLoginViewModel:ViewModel(){
privateval_uiState=MutableStateFlow(UiState())
valuiState:StateFlow<UiState>
get()=_uiState.asStateFlow()
funupdateUsername(username:String)=_uiState.update{it.copy(username=username)}
funupdatePassword(password:String)=_uiState.update{it.copy(password=password)}
}
dataclassUiState(
valusername:String="",
valpassword:String=""
)
@Composable
funLoginForm(
loginViewModel:LoginViewModel,
modifier:Modifier=Modifier
){
valinitialUiState=remember(loginViewModel){loginViewModel.uiState.value}
Column(modifier){
valusernameState=rememberTextFieldState(initialUiState.username)
LaunchedEffect(usernameState){
snapshotFlow{usernameState.text.toString()}.collectLatest{
loginViewModel.updateUsername(it)
}
}
TextField(usernameState)
valpasswordState=rememberTextFieldState(initialUiState.password)
LaunchedEffect(passwordState){
snapshotFlow{passwordState.text.toString()}.collectLatest{
loginViewModel.updatePassword(it)
}
}
SecureTextField(passwordState)
}
}
- Keep your
ViewModelandUiStateclasses the same. - Instead of hoisting the state directly into
ViewModeland making it the source of the truth forTextFields, turnViewModelinto a simple data holder.- To do this, observe the changes to each
TextFieldState.textby collecting asnapshotFlowin aLaunchedEffect.
- To do this, observe the changes to each
- Your
ViewModelwill still have the latest values from UI, but itsuiState: StateFlow<UiState>won't be driving theTextFields. - Any other persistence logic implemented in your
ViewModelcan stay the same.