Building a Custom AutoComplete Component with Jetpack Compose Using BasicTextField & LazyColumn

Ramadan Sayed
6 min readSep 26, 2024

--

In this tutorial, I’ll walk you through creating a Custom AutoComplete Component from scratch using Jetpack Compose. I’ll use BasicTextField to handle user input and LazyColumn to show suggestions that update as the user types. I’ll also add smooth focus handling and animations to make the UI feel polished and interactive.

This component is flexible and can be customized for different uses like search bars, dropdowns, and selection fields. I’ll break down the code step by step, so by the end, you’ll have a working AutoComplete component ready to use in your app.

Breaking Down the Component

1. Handling User Input with BasicTextField

The BasicTextField is the core part of the component where the user enters their input. It allows us to control the text, cursor, and other UI elements with flexibility and minimal setup.

Unlike TextField, which comes with pre-built UI components (like labels, padding, and borders), BasicTextField gives you complete control over styling and behavior.

Here’s how we handle user input:

BasicTextField(
value = textFieldValue,
onValueChange = { newValue ->
if (newValue.text != textFieldValue.text) {
textFieldValue = newValue

// Show suggestions only when input is not empty
showSuggestions = newValue.text.isNotEmpty()
// Filter the suggestions based on the current input
filteredSuggestions = if (newValue.text.isEmpty()) {
suggestions
} else {
suggestions.filter { it.contains(newValue.text, ignoreCase = true) }
}
}
},
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp)
.focusRequester(focusRequester),
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
cursorBrush = SolidColor(Color.Blue),
decorationBox = { innerTextField ->
if (textFieldValue.text.isEmpty()) {
Text(
text = "Enter Fruit...",
style = TextStyle(color = Color.Gray, fontSize = 18.sp)
)
}
innerTextField()
}
)

Key Features of BasicTextField:

  • Complete Control: Unlike the standard TextField, you control how the field looks and behaves.
  • onValueChange Handling: We listen for text changes and update textFieldValue accordingly.
  • Suggestions Logic: As the user types, the showSuggestions flag is toggled and filteredSuggestions is updated to display relevant suggestions.
  • Placeholder Text: The placeholder (“Enter Fruit…”) is displayed only when the input field is empty.

2. Focus Handling with FocusRequester

When the AutoComplete component is displayed, we want the input field to automatically gain focus. This allows users to start typing immediately without manually selecting the field.

We use FocusRequester to programmatically manage the focus:

// Automatically request focus when the UI is composed
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}

The LaunchedEffect hook ensures the focus is requested as soon as the component is launched. This improves the user experience by enabling them to start typing right away.

3. Displaying Suggestions with LazyColumn

Once the user starts typing, we need to display suggestions based on the input. The LazyColumn helps display a vertical list of suggestions efficiently, even if the list is large.

Here’s how we display the filtered suggestions:

LazyColumn(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
items(filteredSuggestions) { suggestion ->
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
// Update text with selected suggestion and move cursor to the end
textFieldValue = TextFieldValue(
text = suggestion,
selection = TextRange(suggestion.length)
)
// Hide suggestions after selection
showSuggestions = false
}
.padding(12.dp)
) {
Text(
text = suggestion,
style = TextStyle(fontSize = 18.sp),
modifier = Modifier.fillMaxWidth()
)
}
}
}

Key Features:

  • LazyColumn: Efficiently renders the list of filtered suggestions.
  • Click Events: When the user clicks on a suggestion, the textFieldValue is updated with that suggestion, and the list is hidden.
  • Filtering: Suggestions are filtered dynamically based on the current input, allowing for real-time feedback to the user.

4. Styling the UI

To make the UI look clean and polished, we add some basic styling using Card and Box components with shadows and rounded corners.

Card(
modifier = Modifier
.fillMaxWidth()
.shadow(8.dp, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(8.dp)
)

This gives the AutoComplete field and suggestion list a modern, polished look with shadows for depth and rounded corners for smoothness.

5. Animating the Suggestion List

We use animateContentSize() to add smooth animation when the suggestion list appears and disappears. This ensures that the list grows and shrinks smoothly based on the number of suggestions.

Column(
modifier = Modifier
.fillMaxWidth()
.shadow(8.dp, RoundedCornerShape(8.dp))
.background(Color.White, shape = RoundedCornerShape(8.dp))
.animateContentSize() // Lazy animation
)

The animateContentSize() function automatically animates changes to the size of the component. This provides a more pleasant experience when suggestions appear or disappear.

Full Code

Here’s the complete code for the AutoComplete TextField component:

@Composable
fun EnhancedAutoCompleteTextField() {
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
val suggestions = listOf(
"Apple", "Banana", "Cherry", "Date", "Grape", "Pineapple",
"Orange", "Blueberry", "Strawberry", "Mango", "Kiwi", "Peach",
"Plum", "Raspberry", "Watermelon", "Cantaloupe", "Papaya", "Lemon",
"Lime", "Apricot", "Blackberry", "Cranberry", "Grapefruit", "Guava",
"Lychee", "Nectarine", "Pomegranate", "Tangerine", "Coconut",
"Dragonfruit", "Passionfruit", "Mulberry", "Jackfruit", "Durian", "Persimmon",
"Fig", "Soursop", "Quince", "Starfruit", "Avocado", "Melon",
"Cucumber", "Clementine", "Honeydew", "Jujube", "Kumquat"
)
var filteredSuggestions by remember { mutableStateOf(suggestions) }
var showSuggestions by remember { mutableStateOf(false) }
// FocusRequester to control focus programmatically
val focusRequester = remember { FocusRequester() }

Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// TextField for user input
Card(
modifier = Modifier
.fillMaxWidth()
.shadow(8.dp, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(8.dp)
) {
BasicTextField(
value = textFieldValue,
onValueChange = { newValue ->
if (newValue.text != textFieldValue.text) {
textFieldValue = newValue
// Show suggestions only when typing and input is not empty
showSuggestions = newValue.text.isNotEmpty()
// Update filtered suggestions based on current input
filteredSuggestions = if (newValue.text.isEmpty()) {
suggestions
} else {
suggestions.filter { it.contains(newValue.text, ignoreCase = true) }
}
}
},
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp)
.focusRequester(focusRequester),
textStyle = TextStyle(color = Color.Black, fontSize = 18.sp),
cursorBrush = SolidColor(Color.Blue),
decorationBox = { innerTextField ->
if (textFieldValue.text.isEmpty()) {
Text(
text = "Enter Fruit...",
style = TextStyle(color = Color.Gray, fontSize = 18.sp)
)
}
innerTextField()
}
)
}
// Automatically request focus when the UI is composed
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.shadow(8.dp, RoundedCornerShape(8.dp))
.background(Color.White, shape = RoundedCornerShape(8.dp))
.animateContentSize()
) {
if (showSuggestions && filteredSuggestions.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
items(
items = filteredSuggestions,
key = { suggestion -> suggestion } // Using the suggestion string as the key
) { suggestion ->
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
// Update text with selected suggestion and move cursor to the end
textFieldValue = TextFieldValue(
text = suggestion,
selection = TextRange(suggestion.length)
)
// Hide suggestions after selection
showSuggestions = false
}
.padding(12.dp)
) {
Text(
text = suggestion,
style = TextStyle(fontSize = 18.sp),
modifier = Modifier.fillMaxWidth()
)
}
}
}
} else if (showSuggestions && filteredSuggestions.isEmpty()) {
Text(
text = "No suggestions available",
style = TextStyle(color = Color.Gray, fontSize = 16.sp),
modifier = Modifier.padding(8.dp)
)
}
}
}
}

Conclusion

In this tutorial, we built a fully functional AutoComplete component with Jetpack Compose. It handles dynamic user input, displays filtered suggestions and updates the text field when a suggestion is selected. Additionally, we styled the UI with rounded corners, shadows, and smooth animations for a better user experience.

Connect with Me on LinkedIn

If you found this article helpful and want to stay updated with more insights and tips on Android development, Jetpack Compose, and other tech topics, feel free to connect with me on LinkedIn. I regularly publish articles, share my experiences, and engage with the developer community. Your feedback and interaction are always welcome!

Follow me on LinkedIn

--

--