Photo by Daniel Romero on Unsplash
Text input is a core part of user interaction in Android apps. With the latest updates announced at Google I/O 2025, Jetpack Compose introduces powerful new text input APIs that transform how developers handle text.
This guide explores these features — from basic setup to advanced capabilities like autofill, secure passwords, smart formatting, and system integration. Compose’s declarative approach makes implementation more intuitive, flexible, and maintainable.
We’ll cover key topics such as state management, security, autofill, and advanced text handling to help you build everything from simple forms to rich text editors with a seamless user experience.
Getting Started: Setting Up Dependencies
Before diving into the implementation details, let’s ensure your project is properly configured to use these new text input features. The capabilities we’ll explore in this article are part of a significant overhaul to Compose’s text APIs, introduced in Compose Foundation 1.8 and Compose Material 3 1.4.0. These powerful features are designed to revolutionize how we handle text input in Android applications.
To incorporate these features into your project, you’ll need to use a recent version of the Jetpack Compose Bill of Materials (BOM). Add the following to your app module’s `build.gradle.kts` file:
dependencies {
// Use a recent BOM version that includes the new text APIs
implementation(platform("androidx.compose:compose-bom:2025.04.01"))
// Individual Compose libraries - versions managed by BOM
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material3:material3")
}
The BOM (Bill of Materials) ensures that all your Compose dependencies are compatible and include the latest APIs. This configuration provides access to all the new text input features we’ll explore throughout this article. The state-based text fields with `rememberTextFieldState` come from Material 3 1.4.0, while features like input and output transformations, the experimental but stable `contentReceiver` modifier for rich content handling, and advanced text sizing with `TextAutoSize` are part of Foundation 1.8.0.
While the example above demonstrates direct dependency declarations for clarity, in production applications, it’s recommended to use a version catalog (`.toml` file) for dependency management. This approach provides better version consistency, easier maintenance, and cleaner project organization across your entire codebase.
With our dependencies properly configured, let’s start by understanding how text input handling has evolved in Compose, beginning with the traditional approach that many existing codebases still use today.
The Legacy Approach: Value-Callback Pattern
Before diving into the latest features announced at Google I/O 2025, it’s important to understand the traditional approach to text input in Compose. The legacy pattern, which is still widely used in existing codebases, relies on a value-callback mechanism where state management is handled through explicit state hoisting
@Composable
private fun LegacyField() { // not deprecated.
val textState = remember { mutableStateOf("") }
TextField(
value = textState.value,
onValueChange = { textState.value = it },
placeholder = { Text("Legacy Field") }
)
}
Early composable required developers to manually manage text state using mutableStateOf, passing it to TextField and updating it via onValueChange. While this offered fine-grained control, it quickly became verbose and repetitive.
As apps grew, developers faced boilerplate-heavy patterns, cumbersome state hoisting, and limited tools for input transformation or consistent text presentation. These challenges led to the creation of the new state-based TextField API, designed to simplify text handling and deliver a more powerful, developer-friendly experience.
The New State-Based Approach: TextFieldState
Introduced at Google I/O 2025, the new state-based TextField API represents a significant evolution in how we handle text input in Compose. This new approach simplifies state management and provides more powerful features out of the box
@Composable
private fun NewStateField() {
val user = rememberTextFieldState()
TextField(
state = user,
placeholder = { Text("New State Field") }
)
}

The new state-based TextField API marks a major shift in Compose, replacing the traditional value-and-callback model with a unified TextFieldState. This approach simplifies state management, reduces boilerplate, and makes sharing state between components easier, establishing it as the best practice for text input.
Assistive Text Support
The assistive texts are also available in the new TextField API, maintaining consistency with older versions while leveraging the new state-based approach. This feature allows developers to provide helpful guidance and error feedback to users:
@Composable
fun AssistiveTextField() {
val user = rememberTextFieldState()
TextField(
state = user,
placeholder = { Text("Assistive State Field") },
supportingText = { Text("This is supporting text") },
isError = user.text.length > 3
)
}

The supportingText parameter displays contextual help or validation feedback below the text field, while isError visually indicates errors—together creating a clearer, more user-friendly input experience.
Text Transformations
One of the most significant improvements in the new TextField API is the introduction of input and output transformations, powered by the TextFieldBufferclass. While text transformations were possible in the legacy approach, the new system provides a more structured and powerful way to manipulate text input and output
@Composable
fun TransformationsField() {
val user = rememberTextFieldState()
TextField(
state = user,
inputTransformation = { filterAsteriskReverting() },
outputTransformation = { transformToUppercase() },
lineLimits = TextFieldLineLimits.SingleLine,
placeholder = { Text("Transformations Field") },
)
}
The new API introduces two types of transformations:
1. Input Transformations: Applied when the user enters text, allowing you to validate, modify, or reject input before it’s stored.
Per the official docs, inputTransformation runs only on user-initiated edits (keyboard, paste/drag, accessibility, tests). It does not run on programmatic state changes, and changing the transformation does not reprocess existing text, it applies on the next user edit.
2. Output Transformations: Applied when displaying text, enabling formatting without modifying the underlying data. These are purely visual; the underlying state is unchanged.
private fun TextFieldBuffer.filterAsteriskReverting() {
val text = originalText.toString()
if (text.contains("*")) {
revertAllChanges() // Reject input containing asterisks
}
}
private fun TextFieldBuffer.transformToUppercase() {
val text = originalText.toString()
replace(0, text.length, text.uppercase())
}

Secure Text Input
The new TextField API introduces a specialized component called SecureTextField, designed specifically for handling sensitive information like passwords. This new component brings significant improvements to security and user experience in password entry scenarios.
@Composable
private fun PasswordField() {
val secureState = rememberTextFieldState()
SecureTextField(
state = secureState,
placeholder = { Text("Password Field") }
)
}

The SecureTextField provides strong, built-in protections for sensitive input. It enforces single-line entry, disables cut, copy, and drag actions, and automatically obscures text to prevent visual leaks.
For usability, it supports displaying password requirements and uses the isError parameter for clear feedback when validation fails—helping users meet security standards while maintaining a smooth experience.
Visual Variants
The SecureTextFieldoffers thoughtful visual customization options to suit different design needs. The default filled variant provides strong visual emphasis, making password fields immediately recognizable within your application’s interface. For designs requiring a more subtle approach, the OutlinedSecureTextField variant offers a lighter visual treatment while maintaining all security features.
@Composable
private fun OutlinedPasswordField() {
val secureState = rememberTextFieldState()
OutlinedSecureTextField(
state = secureState,
placeholder = { Text("Password Field") }
)
}

Text Obfuscation Mode: Flexible Password Visibility
One of the most requested features in password fields is the ability to toggle password visibility. The new API introduces TextObfuscationMode ,providing a more elegant and flexible way to handle password visibility states. Here’s how to implement a password field with a visibility toggle:
@Composable
private fun TextObfuscationTextField() {
var isObfuscated by remember { mutableStateOf(false) }
val user = rememberTextFieldState()
SecureTextField(
state = user,
placeholder = { Text("Text Obfuscation Field") },
trailingIcon = {
IconButton(onClick = { isObfuscated = !isObfuscated }) {
Icon(
imageVector = if (isObfuscated) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (isObfuscated) "Hide password" else "Show password"
)
}
},
textObfuscationMode = if (isObfuscated) TextObfuscationMode.Visible else TextObfuscationMode.Hidden
)
}

TextObfuscationMode gives full control over password visibility: Hidden obscures all characters, Visible shows them, and RevealLastTyped briefly displays each character as typed. This approach ensures secure, consistent behavior, built-in accessibility, and easy switching between modes. Combined with SecureTextField, it offers a complete, user-friendly, and low-effort solution for password input.
Auto-Resizing Text Fields
As it has been talked in my older blog posts, Auto sizing text feature has been added to basic text, which also enables us to build auto resizing text field.
Here’s how to implement an auto-resizing text field:
@Composable
fun AutoResizingTextField() {
val state = rememberTextFieldState("Auto Resizing Field")
BasicTextField(
state = state,
modifier = Modifier.size(120.dp, 64.dp),
decorator = { _ ->
BasicText(
text = state.text.toString(),
style = TextStyle(fontSize = 24.sp),
autoSize = TextAutoSize.StepBased()
)
}
)
}

This powerful feature automatically adjusts the text size to fit within the available space, making it particularly useful for scenarios where the text length is dynamic or unpredictable.
Autofill Support
While we’ll explore Compose’s autofill capabilities in much greater detail in a future blog post, it’s worth taking a moment to understand how easily we can implement basic autofill functionality in our text fields. The [official Android documentation on autofill provides comprehensive coverage of all features, but let’s look at a simple implementation that demonstrates the core concepts.
Autofill is a powerful feature that can significantly improve user experience by automatically populating form fields with saved data. The implementation in Compose is remarkably straightforward:
@Composable
fun AutofillTextField() {
Row(
verticalAlignment = Alignment.CenterVertically
) {
val autofill = LocalAutofillManager.current
val user = rememberTextFieldState()
TextField(
state = user,
modifier = Modifier
.weight(0.5f)
.semantics {
contentType = ContentType.EmailAddress
},
placeholder = { Text("Email Address") }
)
Spacer(modifier = Modifier.padding(4.dp))
Button(
modifier = Modifier.weight(0.5f),
onClick = { autofill?.commit() }
) {
Text("Save")
}
}
}

This implementation showcases seamless autofill integration in Compose. Using LocalAutofillManager.current, it connects the app to Android’s autofill framework, handling all complex interactions behind the scenes.
Once integrated, the system automatically manages suggestions, field population, visual feedback, and keyboard configurations, providing a smooth and reliable user experience. The example serves as a foundation for exploring advanced autofill features like custom highlights, strong password generation, and complex form handling, all detailed in the official documentation.
Rich Content Text Fields
Modern text input often extends beyond simple text entry to include rich content like images, GIFs, and stickers. Compose’s experimental but stable contentReceiver modifier provides a unified approach to handling such content, whether it comes from keyboard input, paste operations, or drag-and-drop interactions. Let’s explore how to create a text field that can handle rich content seamlessly.
The core of rich content handling in Compose revolves around the contentReceivermodifier, which provides a unified API for receiving various types of content. Here’s a basic implementation that demonstrates the concept:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RichContentTextField(
richContentViewModel: RichContentViewModel = viewModel()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.border(1.dp, color = Color.Black)
.contentReceiver(richContentViewModel::handleContent)
) {
LazyRow(
modifier = Modifier.fillMaxWidth()
) {
items(richContentViewModel.selectedImages) { imageUri ->
AsyncImage(
model = imageUri,
contentDescription = null,
modifier = Modifier
.size(64.dp)
.padding(end = 4.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
}
val user = rememberTextFieldState()
TextField(
state = user,
placeholder = { Text("Rich Content Field") },
)
}
}

The strength of this implementation lies in its simplicity. Applying the contentReceiver modifier to the parent Column turns it into a full drop zone for rich content. It supports multiple input methods—keyboard inserts (GIFs, stickers), clipboard pastes, and drag-and-drop for files and images.
At its core, the modifier exposes a TransferableContent object that unifies platform APIs like ClipData and ClipDescription, simplifying content handling. Content processing is delegated to a ViewModel, keeping UI code clean and focused.
The full implementation on GitHub adds advanced features such as content validation, error handling with user feedback, loading indicators, custom styling, and seamless integration with diverse content providers.
Job Offers
Conclusion
Jetpack Compose has once again raised the bar for Android UI development, and its new text input APIs are a testament to this evolution. Throughout this guide, we’ve journeyed from the foundational legacy patterns to the powerful, state-driven approach centered around TextFieldState. This modern API doesn’t just reduce boilerplate; it unlocks a new level of control, security, and user-centric design.
We’ve seen how features like input and output transformations provide a structured way to validate and format data, how SecureTextField offers robust, out-of-the-box security for sensitive information, and how advanced components for OTP entry, autofill, and rich content can be built with surprising simplicity. These tools empower developers to move beyond basic text boxes and craft the kind of intuitive, seamless, and intelligent input experiences that modern users expect.
As you integrate these new capabilities into your applications, you’ll find that you can build more polished, reliable, and feature-rich UIs with less effort than ever before. The future of text input in Compose is not just about typing text into a box — it’s about creating a dynamic and responsive bridge between the user and your app.
This article was previously published on proandroiddev.com



