15

I want to create SearchView using jetpack compose, but I can't found any example that could helps me. Thanks in Advance.

enter image description here

2
  • Which type of view you want can you add screenshot for reference Commented Nov 15, 2019 at 5:32
  • @AnkitMahadik I have attached image to the question Commented Nov 15, 2019 at 16:43

5 Answers 5

31

This is a complex but full implementation for a SearchView from scratch. And the result will be as in the gif below, you can customize or remove InitialResults or Suggestions if you don't want your initial Composable to be displayed when SearchView is not focused and empty

enter image description here

Full implementation is available in github repository.

1- Create search states with

/**
 * Enum class with different values to set search state based on text, focus, initial state and
 * results from search.
 *
 * **InitialResults** represents the initial state before search is initiated. This represents
 * the whole screen
 *
 */
enum class SearchDisplay {
    InitialResults, Suggestions, Results, NoResults
}

2- Then create class where you define your search logic

@Stable
class SearchState(
    query: TextFieldValue,
    focused: Boolean,
    searching: Boolean,
    suggestions: List<SuggestionModel>,
    searchResults: List<TutorialSectionModel>
) {
    var query by mutableStateOf(query)
    var focused by mutableStateOf(focused)
    var searching by mutableStateOf(searching)
    var suggestions by mutableStateOf(suggestions)
    var searchResults by mutableStateOf(searchResults)

    val searchDisplay: SearchDisplay
        get() = when {
            !focused && query.text.isEmpty() -> SearchDisplay.InitialResults
            focused && query.text.isEmpty() -> SearchDisplay.Suggestions
            searchResults.isEmpty() -> SearchDisplay.NoResults
            else -> SearchDisplay.Results
        }

    override fun toString(): String {
        return "🚀 State query: $query, focused: $focused, searching: $searching " +
                "suggestions: ${suggestions.size}, " +
                "searchResults: ${searchResults.size}, " +
                " searchDisplay: $searchDisplay"

    }
}

3- remember state to not update in every composition but only when our seach state changes

@Composable
fun rememberSearchState(
    query: TextFieldValue = TextFieldValue(""),
    focused: Boolean = false,
    searching: Boolean = false,
    suggestions: List<SuggestionModel> = emptyList(),
    searchResults: List<TutorialSectionModel> = emptyList()
): SearchState {
    return remember {
        SearchState(
            query = query,
            focused = focused,
            searching = searching,
            suggestions = suggestions,
            searchResults = searchResults
        )
    }
}

TutorialSectionModel is the model i used it can be generic type T or specific type you wish to display

4- Create a hint to be displayed when not focused

@Composable
private fun SearchHint(modifier: Modifier = Modifier) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxSize()
            .then(modifier)

    ) {
        Text(
            color = Color(0xff757575),
            text = "Search a Tag or Description",
        )
    }
}

I didn't use an Icon but if you wish you can add one

5- Create a SearchTextfield that can has cancel button, CircularProgressIndicator to display loading and BasicTextField to input

/**
 * This is a stateless TextField for searching with a Hint when query is empty,
 * and clear and loading [IconButton]s to clear query or show progress indicator when
 * a query is in progress.
 */
@Composable
fun SearchTextField(
    query: TextFieldValue,
    onQueryChange: (TextFieldValue) -> Unit,
    onSearchFocusChange: (Boolean) -> Unit,
    onClearQuery: () -> Unit,
    searching: Boolean,
    focused: Boolean,
    modifier: Modifier = Modifier
) {

    val focusRequester = remember { FocusRequester() }

    Surface(
        modifier = modifier
            .then(
                Modifier
                    .height(56.dp)
                    .padding(
                        top = 8.dp,
                        bottom = 8.dp,
                        start = if (!focused) 16.dp else 0.dp,
                        end = 16.dp
                    )
            ),
        color = Color(0xffF5F5F5),
        shape = RoundedCornerShape(percent = 50),
    ) {

        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            Box(
                contentAlignment = Alignment.CenterStart,
                modifier = modifier
            ) {

                if (query.text.isEmpty()) {
                    SearchHint(modifier.padding(start = 24.dp, end = 8.dp))
                }

                Row(verticalAlignment = Alignment.CenterVertically) {
                    BasicTextField(
                        value = query,
                        onValueChange = onQueryChange,
                        modifier = Modifier
                            .fillMaxHeight()
                            .weight(1f)
                            .onFocusChanged {
                                onSearchFocusChange(it.isFocused)
                            }
                            .focusRequester(focusRequester)
                            .padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
                        singleLine = true
                    )

                    when {
                        searching -> {
                            CircularProgressIndicator(
                                modifier = Modifier
                                    .padding(horizontal = 6.dp)
                                    .size(36.dp)
                            )
                        }
                        query.text.isNotEmpty() -> {
                            IconButton(onClick = onClearQuery) {
                                Icon(imageVector = Icons.Filled.Cancel, contentDescription = null)
                            }
                        }
                    }
                }
            }
        }

    }
}

You can remove CircularProgressBar or add Icon to Row which contains BasicTextField

6- SearchBar with SearchTextField above and back arrow to return back feature with. AnimatedVisibility makes sure arrow is animated when we focus BasicTextField in SearchTextField, it can also be used with Icon as magnifying glass.

@ExperimentalAnimationApi
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SearchBar(
    query: TextFieldValue,
    onQueryChange: (TextFieldValue) -> Unit,
    onSearchFocusChange: (Boolean) -> Unit,
    onClearQuery: () -> Unit,
    onBack: ()-> Unit,
    searching: Boolean,
    focused: Boolean,
    modifier: Modifier = Modifier
) {

    val focusManager = LocalFocusManager.current
    val keyboardController = LocalSoftwareKeyboardController.current

    Row(
        modifier = modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {

        AnimatedVisibility(visible = focused) {
            // Back button
            IconButton(
                modifier = Modifier.padding(start =2.dp),
                onClick = {
                focusManager.clearFocus()
                keyboardController?.hide()
                    onBack()
            }) {
                Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
            }
        }

        SearchTextField(
            query,
            onQueryChange,
            onSearchFocusChange,
            onClearQuery,
            searching,
            focused,
            modifier.weight(1f)
        )
    }
}

7- To use SearchBar create a rememberSearchState and update state as Column is used here because rest of the screen is updated based on SearchState

LaunchedEffect or setting mutableState in ViewModel can be used to set query result or searching field of state to display loading

@Composable
fun HomeScreen(
    modifier: Modifier = Modifier,
    viewModel: HomeViewModel,
    navigateToTutorial: (String) -> Unit,
    state: SearchState = rememberSearchState()
) {


    Column(
        modifier = modifier.fillMaxSize()
    ) {
            
        SearchBar(
            query = state.query,
            onQueryChange = { state.query = it },
            onSearchFocusChange = { state.focused = it },
            onClearQuery = { state.query = TextFieldValue("") },
            onBack = { state.query = TextFieldValue("") },
            searching = state.searching,
            focused = state.focused,
            modifier = modifier
        )

        LaunchedEffect(state.query.text) {
            state.searching = true
            delay(100)
            state.searchResults = viewModel.getTutorials(state.query.text)
            state.searching = false
        }

        when (state.searchDisplay) {
            SearchDisplay.InitialResults -> {

            }
            SearchDisplay.NoResults -> {

            }

            SearchDisplay.Suggestions -> {

            }

            SearchDisplay.Results -> {
 
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

4

This is the SearchView you have in that image :

val (value, onValueChange) = remember { mutableStateOf("") }

    TextField(
          value = value,
          onValueChange = onValueChange,
          textStyle = TextStyle(fontSize = 17.sp),
          leadingIcon = { Icon(Icons.Filled.Search, null, tint = Color.Gray) },
                modifier = Modifier
                    .padding(10.dp)
                    .background(Color(0xFFE7F1F1), RoundedCornerShape(16.dp)),
                placeholder = { Text(text = "Bun") },
                colors = TextFieldDefaults.textFieldColors(
                    focusedIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent,
                    backgroundColor = Color.Transparent,
                    cursorColor = Color.DarkGray
                )
            )

Comments

2
TextField(
   startingIcon = Icon(bitmap = searchIcon),
   placeholder = { Text(...) }
)

Comments

0

I wanted something simple so:

@Composable
fun SearchBox(items: List<String>, onClick: (String)->Unit){
    val sortedList = items.sortedBy { it } //sort alphabetically
    OutlinedCard(Modifier.fillMaxWidth(),RoundedCornerShape(20.dp), CardDefaults.cardColors()) {
        var searchList by remember { mutableStateOf(emptyList<String>()) }
        var searchQuery by remember { mutableStateOf("") }
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth().padding(10.dp), shape = RoundedCornerShape(10.dp), colors = TextFieldDefaults.colors(), leadingIcon = {Icon(Icons.Filled.Search, "Search")},
            maxLines = 1, singleLine = true, value = searchQuery, onValueChange = {
                searchQuery = it
                searchList = if (searchQuery.isBlank()) emptyList() else sortedList.filter { item -> item.lowercase(Locale.getDefault()).contains(searchQuery.lowercase(Locale.getDefault())) }
            }, placeholder = { Text(text = "Search here")}
        )
        searchList.forEach{
            Text(it, Modifier.fillMaxWidth().padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 10.dp).clickable {
                searchQuery = ""
                searchList = emptyList()
                onClick(it)
            })
            Divider(thickness = 1.dp, color = Color.DarkGray)
        }
    }
}

Comments

-1

Just create component, with FlexRow if you want to create UI like those.

FlexRow(crossAxisAlignment = CrossAxisAlignment.Start) {
    inflexible {
        drawImageResource(R.drawable.image_search)

    }
    expanded(1.0f) {
        SingleLineEditText(
            state,
            keyboardType = KeyboardType.Text,
            imeAction = ImeAction.Search,
            editorStyle = EditorStyle(textStyle = TextStyle(fontSize = 16.sp)),
            onImeActionPerformed = {
                onSearch(state.value.text)
            }
        )
    }
}

3 Comments

FlexRow has now been deprecated so use Row instead.
@VinayGaba But Row doesn't have any expanded or flex functionality, what should be the replacement for that?
@AhmedAli Use Row and weight modifier for SingleLineEditText

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.