How to Create a Search View in Jetpack Compose: A Step-by-Step Guide

How to Create a Search View in Jetpack Compose: A Step-by-Step Guide

Learn How to Create a Custom Search View in Jetpack Compose

I recently came across a challenge when I was making an app completely using Jetpack Compose, if you haven't heard about it check it out. I needed a Search View, but there was no out-of-the-box solution for it so I created my own. Here's is how it looked like:

To create a search view in Jetpack Compose, we need to define three functions: ExpandableSearchView, CollapsedSearchView, and ExpandedSearchView. The ExpandableSearchView function will contain the logic for expanding and collapsing the search view, while the CollapsedSearchView and ExpandedSearchView functions will define the appearance and behavior of the search view when it is collapsed and expanded, respectively.

First, let's define the ExpandableSearchView function. It should take the following arguments:

  • searchDisplay: This is the string that is displayed in the search field when it is collapsed.

  • onSearchDisplayChanged: This is a lambda function that is called when the search display is changed. You can use this function to update the UI or perform a search based on the new search display.

  • onSearchDisplayClosed: This is a lambda function that is called when the search display is closed. You can use this function to reset the search display or clear the search results.

  • modifier: This is an optional argument that allows you to specify a Modifier for the search view.

  • expandedInitially: This is an optional argument that specifies whether the search view should be expanded initially. The default value is false.

  • tint: This is an optional argument that specifies the color of the search icon and the text in the search field. The default value is the onPrimary color from the MaterialTheme.

Here's the code for the ExpandableSearchView function:

@Composable
fun ExpandableSearchView(
    searchDisplay: String,
    onSearchDisplayChanged: (String) -> Unit,
    onSearchDisplayClosed: () -> Unit,
    modifier: Modifier = Modifier,
    expandedInitially: Boolean = false,
    tint: Color = MaterialTheme.colors.onPrimary
) {
    val (expanded, onExpandedChanged) = remember {
        mutableStateOf(expandedInitially)
    }


    Crossfade(targetState = expanded) { isSearchFieldVisible ->
        when (isSearchFieldVisible) {
            true -> ExpandedSearchView(
                searchDisplay = searchDisplay,
                onSearchDisplayChanged = onSearchDisplayChanged,
                onSearchDisplayClosed = onSearchDisplayClosed,
                onExpandedChanged = onExpandedChanged,
                modifier = modifier,
                tint = tint
            )

            false -> CollapsedSearchView(
                onExpandedChanged = onExpandedChanged,
                modifier = modifier,
                tint = tint
            )
        }
    }
}

The ExpandableSearchView function uses a Crossfade component to switch between the collapsed and expanded states of the search view. When the search view is collapsed, it displays the CollapsedSearchView component. When the search view is expanded, it displays the ExpandedSearchView component.

Next, we need to define the CollapsedSearchView and ExpandedSearchView components.

The CollapsedSearchView component is a simple row that displays a text label and a search icon button. When the search icon button is clicked, the CollapsedSearchView component calls the onExpandedChanged lambda function to expand the search view.

Here is the code for the CollapsedSearchView component:

@Composable
fun CollapsedSearchView(
    onExpandedChanged: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    tint: Color = MaterialTheme.colors.onPrimary,
) {

    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(vertical = 2.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "Tasks",
            style = MaterialTheme.typography.h6,
            modifier = Modifier
                .padding(start = 16.dp)
        )
        IconButton(onClick = { onExpandedChanged(true) }) {
            SearchIcon(iconTint = tint)
        }
    }
}

The ExpandedSearchView component is a row that contains a back icon button and a text field for entering the search query. The text field is bound to the searchDisplay variable, so any changes to the search query will be reflected in the searchDisplay variable and the onSearchDisplayChanged lambda function will be called. When the user clicks the back icon button or hits the "done" button on the keyboard, the ExpandedSearchView component calls the onSearchDisplayClosed lambda function and collapses the search view.

Here is the code for the ExpandedSearchView component:

@Composable
fun ExpandedSearchView(
    searchDisplay: String,
    onSearchDisplayChanged: (String) -> Unit,
    onSearchDisplayClosed: () -> Unit,
    onExpandedChanged: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    tint: Color = MaterialTheme.colors.onPrimary,
) {
    val focusManager = LocalFocusManager.current

    val textFieldFocusRequester = remember { FocusRequester() }

    SideEffect {
        textFieldFocusRequester.requestFocus()
    }

    var textFieldValue by remember {
        mutableStateOf(TextFieldValue(searchDisplay, TextRange(searchDisplay.length)))
    }

    Row(
        modifier = modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Start,
        verticalAlignment = Alignment.CenterVertically
    ) {
        IconButton(onClick = {
            onExpandedChanged(false)
            onSearchDisplayClosed()
        }) {
            Icon(
                painter = painterResource(id = R.drawable.ic_back),
                contentDescription = "back icon",
                tint = tint
            )
        }
        TextField(
            value = textFieldValue,
            onValueChange = {
                textFieldValue = it
                onSearchDisplayChanged(it.text)
            },
            trailingIcon = {
                SearchIcon(iconTint = tint)
            },
            modifier = Modifier
                .fillMaxWidth()
                .focusRequester(textFieldFocusRequester),
            label = {
                Text(text = "Search", color = tint)
            },
            keyboardOptions = KeyboardOptions(
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions(
                onDone = {
                    focusManager.clearFocus()
                }
            )
        )
    }
}

Finally, we need to define a SearchIcon function that creates an icon for the search view. This function takes a iconTint argument to specify the color of the icon.

@Composable
fun SearchIcon(iconTint: Color) {
    Icon(
        painter = painterResource(id = R.drawable.ic_search),
        contentDescription = "search icon",
        tint = iconTint
    )
}

With these functions defined, we can now use the ExpandableSearchView to create an expandable search view in Jetpack Compose. To do this, simply call the ExpandableSearchView function and pass the necessary arguments.

That's it! You should now have a working expandable search view in Jetpack Compose. You can customize the appearance and behavior of the search view by modifying the ExpandableSearchView, CollapsedSearchView, and ExpandedSearchView functions as needed.

I hope this tutorial has helped you understand how to create an expandable search view in Jetpack Compose. If you have any questions or comments, feel free to leave them below.