Hey there! Welcome to my blog.

Let’s implement TextField and when user submits the query we will update list with matching suggestions. Let’s get started.

You can take reference of the source code from github repository.

1. Add dependencies

We will make use of LiveData and ViewModel to observe the state of data to update the list.
Right now I’m using compose version 1.2.0-beta02 which is latest release.

implementation "androidx.compose.runtime:runtime-livedata:1.2.0-beta02"

2. Search screen design

If you want to build same design from the preview I have shown, make sure to to change app theme and colors similar to mine first.

Color.kt :

// Dark theme
val dark_primary = Color(0xFFffb59c)
val dark_onPrimary = Color(0xFF5f1600)
val dark_background = Color(0xFF211a18)
val dark_onBackground = Color(0xFFede0dc)
val dark_surface = Color(0xFF302522)
val dark_onSurface = Color(0xFFede0dc)

Theme.kt :

val DarkColorPalette = darkColors(
    primary = dark_primary,
    onPrimary = dark_onPrimary,
    background = dark_background,
    onBackground = dark_onBackground,
    surface = dark_surface,
    onSurface = dark_onSurface
)

SearchScreen.kt :

Entry point for search screen with TextField inside TopAppBar.

@Composable
fun SearchScreen() {
    Surface(
        color = MaterialTheme.colors.background
    ) {
        Scaffold(
            // SearchAppBar()
            topBar = { }
        ) {
            // List of actors
            Box { }
        }
    }
}

theme.xml :

To change the color of status bar and navigation bar do it from themes.xml file.

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="android:statusBarColor">#211a18</item>
    <item name="android:navigationBarColor">#211a18</item>
</style>

3. TextField theming

Let’s start by making changes to the TextField to make it look exactly like below one.

var query: String by rememberSaveable { mutableStateOf("") }

TextField(
    value = query,
    onValueChange = { },
    maxLines = 1,
    textStyle = MaterialTheme.typography.subtitle1,
    singleLine = true,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
)

Complete transparent background :

Change background color of TextField to transparent so that the color of shape which will be Rectangle will become visible.

colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent)

Background with new color and shape :

modifier = Modifier
    .background(color = MaterialTheme.colors.background, shape = RectangleShape)

A placeholder text for providing hint :

placeholder = { Text(text = stringResource(R.string.hint_search_query)) }

Leading Icon :

leadingIcon = {
    Icon(
        imageVector = Icons.Rounded.Search,
        tint = MaterialTheme.colors.onBackground,
        contentDescription = "Search Icon"
    )
}

Trailing Icon :

We will add a clear icon to empty the search query submitted by user in TextField.

trailingIcon = {
    IconButton(onClick = { }) {
        Icon(
            imageVector = Icons.Rounded.Clear,
            tint = MaterialTheme.colors.onBackground,
            contentDescription = "Clear Icon"
        )
    }
}

After assigning all these properties to TextField.

var query: String by rememberSaveable { mutableStateOf("") }

TextField(
    value = query,
    onValueChange = { onQueryChanged ->
        query = onQueryChanged
        if (onQueryChanged.isNotEmpty()) {
            // Perform search
        }
    },
    leadingIcon = {
        Icon(
            imageVector = Icons.Rounded.Search,
            tint = MaterialTheme.colors.onBackground,
            contentDescription = "Search icon"
        )
    },
    trailingIcon = {
        if (showClearIcon) {
            IconButton(onClick = { }) {
                Icon(
                    imageVector = Icons.Rounded.Clear,
                    tint = MaterialTheme.colors.onBackground,
                    contentDescription = "Clear icon"
                )
            }
        }
    },
    maxLines = 1,
    colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
    placeholder = { Text(text = stringResource(R.string.hint_search_query)) },
    textStyle = MaterialTheme.typography.subtitle1,
    singleLine = true,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
    modifier = Modifier
        .fillMaxWidth()
        .background(color = MaterialTheme.colors.background, shape = RectangleShape)
)

At this stage our TextField is ready. Now we have to setup the ViewModel and list data.
In ViewModel we will write a function which performs search and updates the list with LiveData.

4. ViewModel to handle data changes

Since we will only show name of the actor in each item of the list, make a data class with single property in it.

Actors.kt :

data class Actors(
    val actorName: String,
)

From the LiveData we will observe the data changes and then update the list in screen with matching query changes submitted by user in TextField.

SearchViewModel.kt :

Declare a LiveData which holds our actors list data.

private var _list = MutableLiveData<List<Actors>>()
val list: LiveData<List<Actors>>
    get() = _list

Create a function which returns list of actors.
Below function has list of only three actors, add more actors by yourself or get more actors from my gist.

private fun actorsListData(): List<Actors> {
    val data = listOf("Adele Exarchopoulos", "timothee chalamet", "Al Pacino")
    val actorsList = ArrayList<Actors>()
    data.forEach {
        actorsList.add(Actors(it))
    }
    return actorsList
}

With data in hand let’s update the LiveData with the list in ViewModel.

init {
    loadActors()
}

fun loadActors() {
    _list.postValue(actorsListData())
}

Finally add a function which receives submitted query by user from TextField.
Filter the list with query. Save the matching items in new ArrayList so that we can use them, we are naming it filteredList.

fun performQuery(
    query: String,
) {
    // New empty array list which contains filtered list with query.
    val filteredList = ArrayList<Actors>()
    // Loop into each actors data to read actors name.
    actorsListData().forEach { actors ->
        // Compare query with actors name to find a match.
        if (actors.actorName.lowercase().contains(query.lowercase())) {
            // If match is found, add that name to filtered list.
            filteredList.add(Actors(actors.actorName))
        }
    }
    // Post updated list to existing LiCeData
    _list.postValue(filteredList)
}

This is updated SearchViewModel class after making those changes.

Now the LiveData from ViewModel class always holds list of actors data by default and when user submits the query, it holds filtered list of actors simultaneously.

5. Observe LiveData changes as State

With observeAsState( ) we can observe LiveData changes as a state from the ViewModel.

val viewModel: SearchViewModel = viewModel()
val actorsList = viewModel.list.observeAsState().value

LazyColumn {
    if (!actorsList.isNullOrEmpty()) {
        items(actorsList) { actor ->
            Text(
                text = actor.actorName,
                style = MaterialTheme.typography.h6,
                color = MaterialTheme.colors.onBackground,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 20.dp, vertical = 12.dp)
                    .wrapContentWidth(Alignment.Start)
            )
        }
    }
}

At this point you will be able to see list of actors with names on the screen.

Once user make changes in the TextField, onValueChange callback will be triggered with the updated query value. Let’s call function performQuery( ) from ViewModel to perform search operation.

We have to use that query to filter the list from TextField.

onValueChange = { onQueryChanged ->
    // If user makes changes to text, immediately update it.
    query = onQueryChanged
    // Perform query only when string isn't empty.
    if (onQueryChanged.isNotEmpty()) {
        // Pass latest query to refresh search results.
        viewModel.performQuery(onQueryChanged)
    }
}

Validating Search Query :

If the query is empty, we should immediately update the LiveData to hold default actors list data.

if (query.isEmpty()) {
    viewModel.loadActors()
}

Validating Trailing Icon

Clear icon which is training icon should be hidden if query is empty.
When user clicks the clear icon, query will be reset to empty String and default list will be visible.

Declare a property to remember state of icon by tracking query value with boolean.

var showClearIcon by rememberSaveable { mutableStateOf(false) }

If query is not empty let’s show the trailing icon, if not it will be hidden by setting value to false.

if (query.isEmpty()) {
    showClearIcon = false
} else if (query.isNotEmpty()) {
    showClearIcon = true
}

Now finally use that state value and update trainingIcon changes.
Reset the query to empty String when user clicks the icon.

trailingIcon = {
    if (showClearIcon) {
        IconButton(onClick = { query = "" }) {
            Icon(
                imageVector = Icons.Rounded.Clear,
                tint = MaterialTheme.colors.onBackground,
                contentDescription = "Clear Icon"
            )
        }
    }
}

After making all those changes here’s how it should look.

SearchScreen.kt :

That’s it guys, thanks for reading and do check my GitHub from below you find more useful repositories.

6. Project code and resources

1. TextField with compose – Android documentation
2. ViewModels with Composables
3. State and Jetpack Compose

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.