TextFieldState


The editable text state of a text field, including both the text itself and position of the cursor or selection.

To change the text field contents programmatically, call edit, setTextAndSelectAll, setTextAndPlaceCursorAtEnd, or clearText. Individual parts of the state like text, selection, or composition can be read from any snapshot restart scope like Composable functions. To observe these members from outside a restart scope, use snapshotFlow { textFieldState.text } or snapshotFlow { textFieldState.selection }.

When instantiating this class from a composable, use rememberTextFieldState to automatically save and restore the field state. For more advanced use cases, pass TextFieldState.Saver to rememberSaveable.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp

class SearchViewModel(val searchFieldState: TextFieldState = TextFieldState()) {
    private val queryValidationRegex = """\w+""".toRegex()

    // Use derived state to avoid recomposing every time the text changes, and only recompose
    // when the input becomes valid or invalid.
    val isQueryValid by derivedStateOf {
        // This lambda will be re-executed every time inputState.text changes.
        searchFieldState.text.matches(queryValidationRegex)
    }

    var searchResults: List<String> by mutableStateOf(emptyList())
        private set

    /** Called while the view model is active, e.g. from a LaunchedEffect. */
    suspend fun run() {
        snapshotFlow { searchFieldState.text }
            .collectLatest { queryText ->
                // Start a new search every time the user types something valid. If the previous
                // search is still being processed when the text is changed, it will be
                // cancelled
                // and this code will run again with the latest query text.
                if (isQueryValid) {
                    searchResults = performSearch(query = queryText)
                }
            }
    }

    fun clearQuery() {
        searchFieldState.setTextAndPlaceCursorAtEnd("")
    }

    private suspend fun performSearch(query: CharSequence): List<String> {
        TODO()
    }
}

@Composable
fun SearchScreen(viewModel: SearchViewModel) {
    Column {
        Row {
            BasicTextField(viewModel.searchFieldState)
            IconButton(onClick = { viewModel.clearQuery() }) {
                Icon(Icons.Default.Clear, contentDescription = "clear search query")
            }
        }
        if (!viewModel.isQueryValid) {
            Text("Invalid query", style = TextStyle(color = Color.Red))
        }
        LazyColumn { items(viewModel.searchResults) { TODO() } }
    }
}

Summary

Nested types

Saves and restores a TextFieldState for rememberSaveable.

Public constructors

@RememberInComposition
TextFieldState(initialText: String, initialSelection: TextRange)
Cmn

Public functions

inline Unit
edit(block: TextFieldBuffer.() -> Unit)

Runs block with a mutable version of the current state.

Cmn
open String
Cmn

Public properties

TextRange?

The current composing range dictated by the IME.

Cmn
TextRange

The current selection range.

Cmn
CharSequence

The current text content.

Cmn
TextFieldTextStyles

Provides access to the styles applied to the text within this TextFieldState.

Cmn
UndoState

Undo history controller for this TextFieldState.

Cmn

Extension functions

Unit

Deletes all the text in the state.

Cmn
Unit

Sets the text in this TextFieldState to text, replacing any text that was previously there, and places the cursor at the end of the new text.

Cmn
Unit

Sets the text in this TextFieldState to text, replacing any text that was previously there, and selects all the text.

Cmn
TextFieldBuffer

Creates a temporary, mutable TextFieldBuffer representing the current state of this TextFieldState.

Cmn

Public constructors

TextFieldState

@RememberInComposition
TextFieldState(
    initialText: String = "",
    initialSelection: TextRange = TextRange(initialText.length)
)

Public functions

edit

inline fun edit(block: TextFieldBuffer.() -> Unit): Unit

Runs block with a mutable version of the current state. The block can make changes to the text and cursor/selection. See the documentation on TextFieldBuffer for a more detailed description of the available operations.

Make sure that you do not make concurrent calls to this function or call it again inside block's scope. Doing either of these actions will result in triggering an IllegalStateException.

import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.insert
import androidx.compose.material.Text
import androidx.compose.ui.text.TextRange

val state = TextFieldState("hello world!")
state.edit {
    // Insert a comma after "hello".
    insert(5, ",") // = "hello, world!"

    // Delete the exclamation mark.
    delete(12, 13) // = "hello, world"

    // Add a different name.
    append("Compose") // = "hello, Compose"

    // Say goodbye.
    replace(0, 5, "goodbye") // "goodbye, Compose"

    // Select the new name so the user can change it by just starting to type.
    selection = TextRange(9, 16) // "goodbye, ̲C̲o̲m̲p̲o̲s̲e"
}

toString

open fun toString(): String

Public properties

composition

val compositionTextRange?

The current composing range dictated by the IME. If null, there is no composing region.

To observe changes to this property outside a restartable function, use snapshotFlow { composition }.

selection

val selectionTextRange

The current selection range. If the selection is collapsed, it represents cursor location. This value will automatically update when the user enters text or otherwise changes the text field selection range. To change it programmatically, call edit.

To observe changes to this property outside a restartable function, use snapshotFlow { selection }.

text

val textCharSequence

The current text content. This value will automatically update when the user enters text or otherwise changes the text field contents. To change it programmatically, call edit.

To observe changes to this property outside a restartable function, use snapshotFlow { text }.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.unit.sp

class SearchViewModel {
    val searchFieldState = TextFieldState()
    var searchResults: List<String> by mutableStateOf(emptyList())
        private set

    /** Called while the view model is active, e.g. from a LaunchedEffect. */
    suspend fun run() {
        snapshotFlow { searchFieldState.text }
            // Let fast typers get multiple keystrokes in before kicking off a search.
            .debounce(500)
            // collectLatest cancels the previous search if it's still running when there's a
            // new change.
            .collectLatest { queryText -> searchResults = performSearch(query = queryText) }
    }

    private suspend fun performSearch(query: CharSequence): List<String> {
        TODO()
    }
}

@Composable
fun SearchScreen(viewModel: SearchViewModel) {
    Column {
        BasicTextField(viewModel.searchFieldState)
        LazyColumn { items(viewModel.searchResults) { TODO() } }
    }
}
See also
edit
snapshotFlow

textStyles

val textStylesTextFieldTextStyles

Provides access to the styles applied to the text within this TextFieldState.

Use this property when you only need to read or observe the current text styles, such as updating a formatting toolbar based on the style of the currently selected text. If you need to modify the text or its styles, use the edit method instead, which provides both read and write access via TextFieldBuffer.

To observe changes to this property outside a restartable function, use snapshotFlow { textStyles }.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.ExpandPolicy
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.Button
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// This sample demonstrates a realistic rich-text editor scenario using the `TrackedRange` and
// `TextFieldTextStyles` APIs. It implements a "Toggle Bold" formatting function on the current
// selection.

// For simplicity, this sample keeps bold styles non-overlapping and contiguous, assuming they
// are
// applied exclusively through this method.
val state = rememberTextFieldState("Hello World")

// This derived state calculates whether the current selection is completely covered by
// bold text styles. This ensures the "Bold" toggle button accurately reflects the
// state of the selected text.
val isSelection100PercentBold by derivedStateOf {
    val selection = state.selection
    if (selection.collapsed) {
        false
    } else {
        val spanStyles = state.textStyles.getSpanStyles(selection.min, selection.max)
        var boldCoverage = 0
        for (style in spanStyles) {
            if (style.item.fontWeight == FontWeight.Bold) {
                val overlapStart = maxOf(style.start, selection.min)
                val overlapEnd = minOf(style.end, selection.max)
                if (overlapEnd > overlapStart) {
                    boldCoverage += (overlapEnd - overlapStart)
                }
            }
        }
        boldCoverage == selection.length
    }
}

fun TextFieldBuffer.unBoldSelection() {
    // Query existing bold styles in the selection
    val intersectingStyles =
        getSpanStyles(selection.min, selection.max).filter {
            it.spanStyle.fontWeight == FontWeight.Bold
        }
    // We modify or remove existing styles to exclude the selected range
    for (style in intersectingStyles) {
        val range = style.textRange
        if (range.start >= selection.min && range.end <= selection.max) {
            // The style is fully inside the selection. Remove it.
            removeStyle(style)
        } else if (range.start < selection.min && range.end > selection.max) {
            // The style completely covers the selection. We need to split it.
            val oldEnd = range.end
            // Truncate the start part
            style.textRange = TextRange(range.start, selection.min)
            // Add a new style for the end part
            addStyle(
                SpanStyle(fontWeight = FontWeight.Bold),
                TextRange(selection.max, oldEnd),
                ExpandPolicy.AtEnd,
            )
        } else if (range.start < selection.min) {
            // The style overlaps with the start of the selection. Truncate it.
            style.textRange = TextRange(range.start, selection.min)
        } else {
            // The style overlaps with the end of the selection. Truncate it.
            style.textRange = TextRange(selection.max, range.end)
        }
    }
}

fun TextFieldBuffer.boldSelection() {
    // Query existing bold styles in the selection
    val intersectingStyles =
        getSpanStyles(selection.min, selection.max).filter {
            it.spanStyle.fontWeight == FontWeight.Bold
        }
    // To keep bold styles non-overlapping, we merge any intersecting bold
    // styles with the new selection range into a single contiguous bold style.
    var mergedStart = selection.min
    var mergedEnd = selection.max

    for (style in intersectingStyles) {
        mergedStart = minOf(mergedStart, style.textRange.start)
        mergedEnd = maxOf(mergedEnd, style.textRange.end)
        // Remove the fragmented style
        removeStyle(style)
    }

    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        TextRange(mergedStart, mergedEnd),
        ExpandPolicy.AtEnd,
    )
}

Column {
    Button(
        onClick = {
            state.edit {
                val selection = this.selection
                if (selection.collapsed) return@edit
                if (isSelection100PercentBold) {
                    unBoldSelection()
                } else {
                    boldSelection()
                }
            }
        }
    ) {
        Text(
            "B",
            fontWeight = if (isSelection100PercentBold) FontWeight.Bold else FontWeight.Normal,
        )
    }

    BasicTextField(state = state, textStyle = LocalTextStyle.current)
}

undoState

@ExperimentalFoundationApi
val undoStateUndoState

Undo history controller for this TextFieldState.

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

val state = rememberTextFieldState()

Column(Modifier.padding(8.dp)) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
        androidx.compose.material.Button(
            onClick = { state.undoState.undo() },
            enabled = state.undoState.canUndo,
        ) {
            Text("Undo")
        }

        androidx.compose.material.Button(
            onClick = { state.undoState.redo() },
            enabled = state.undoState.canRedo,
        ) {
            Text("Redo")
        }

        androidx.compose.material.Button(
            onClick = { state.undoState.clearHistory() },
            enabled = state.undoState.canUndo || state.undoState.canRedo,
        ) {
            Text("Clear History")
        }
    }

    BasicTextField(
        state = state,
        modifier =
            Modifier.fillMaxWidth()
                .border(1.dp, Color.LightGray, RoundedCornerShape(6.dp))
                .padding(8.dp),
        textStyle = TextStyle(fontSize = 16.sp),
    )
}

Extension functions

TextFieldState.clearText

fun TextFieldState.clearText(): Unit

Deletes all the text in the state.

To perform more complicated edits on the text, call TextFieldState.edit. This function is equivalent to calling:

edit {
delete(0, length)
placeCursorAtEnd()
}

TextFieldState.setTextAndPlaceCursorAtEnd

fun TextFieldState.setTextAndPlaceCursorAtEnd(text: String): Unit

Sets the text in this TextFieldState to text, replacing any text that was previously there, and places the cursor at the end of the new text.

To perform more complicated edits on the text, call TextFieldState.edit. This function is equivalent to calling:

edit {
replace(0, length, text)
placeCursorAtEnd()
}

TextFieldState.setTextAndSelectAll

fun TextFieldState.setTextAndSelectAll(text: String): Unit

Sets the text in this TextFieldState to text, replacing any text that was previously there, and selects all the text.

To perform more complicated edits on the text, call TextFieldState.edit. This function is equivalent to calling:

edit {
replace(0, length, text)
selectAll()
}

TextFieldState.toTextFieldBuffer

fun TextFieldState.toTextFieldBuffer(): TextFieldBuffer

Creates a temporary, mutable TextFieldBuffer representing the current state of this TextFieldState.

Use a TextFieldBuffer to:

  • Apply transformations for testing purposes

  • Preview how the TextField would render with a specific OutputTransformation

This is similar to calling TextFieldState.edit, but without committing the changes back to the TextFieldState.

Important: A TextFieldBuffer is intended for short-term use. Let the garbage collector dispose of it when you're finished to avoid unnecessary memory usage.

import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.insert
import androidx.compose.foundation.text.input.toTextFieldBuffer
import androidx.compose.material.Text

val state = TextFieldState("Hello, World")
val outputTransformation = OutputTransformation { insert(0, "> ") }

val buffer = state.toTextFieldBuffer()
with(outputTransformation) { buffer.transformOutput() }

val transformedText = buffer.asCharSequence()
val transformedSelection = buffer.selection