Building a Custom AutoComplete Component with Jetpack Compose Using BasicTextField & LazyColumn
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 updatetextFieldValue
accordingly.- Suggestions Logic: As the user types, the
showSuggestions
flag is toggled andfilteredSuggestions
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!