검색창

검색창을 사용하여 검색 기능을 구현합니다. 검색창은 사용자가 키워드나 구문을 입력하여 앱 내에서 관련성 높은 결과를 표시할 수 있는 영구 검색창으로, 검색이 앱의 주요 기능인 경우에 권장됩니다.

검색창 두 개가 표시됩니다. 왼쪽에는 텍스트 필드만 있습니다.
  왼쪽 검색창에는 텍스트 입력란과 그 아래에 검색 추천이 있습니다.
그림 1. 기본 검색창 (1)과 추천 검색어가 있는 검색창 (2)

API 노출 영역

SearchBar 컴포저블을 사용하여 검색창을 구현합니다. 이 컴포저블의 주요 매개변수는 다음과 같습니다.

  • inputField: 검색창의 입력란을 정의합니다. 일반적으로 다음을 맞춤설정할 수 있는 SearchBarDefaults.InputField를 활용합니다.
    • query: 입력란에 표시할 쿼리 텍스트입니다.
    • onQueryChange: 쿼리 문자열의 변경사항을 처리하는 Lambda입니다.
  • expanded: 검색창이 추천 또는 필터링된 결과를 표시하도록 펼쳐져 있는지 여부를 나타내는 불리언입니다.
  • onExpandedChange: 드롭다운의 펼쳐진 상태 변경을 처리하는 Lambda입니다.

  • content: inputField 아래에 검색 결과를 표시할 검색창의 콘텐츠입니다.

이 스니펫은 추천이 포함된 SearchBar의 기본 구현을 보여줍니다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleSearchBar(
    textFieldState: TextFieldState,
    onSearch: (String) -> Unit,
    searchResults: List<String>,
    modifier: Modifier = Modifier
) {
    // Controls expansion state of the search bar
    var expanded by rememberSaveable { mutableStateOf(false) }

    Box(
        modifier
            .fillMaxSize()
            .semantics { isTraversalGroup = true }
    ) {
        SearchBar(
            modifier = Modifier
                .align(Alignment.TopCenter)
                .semantics { traversalIndex = 0f },
            inputField = {
                SearchBarDefaults.InputField(
                    query = textFieldState.text.toString(),
                    onQueryChange = { textFieldState.edit { replace(0, length, it) } },
                    onSearch = {
                        onSearch(textFieldState.text.toString())
                        expanded = false
                    },
                    expanded = expanded,
                    onExpandedChange = { expanded = it },
                    placeholder = { Text("Search") }
                )
            },
            expanded = expanded,
            onExpandedChange = { expanded = it },
        ) {
            // Display search results in a scrollable column
            Column(Modifier.verticalScroll(rememberScrollState())) {
                searchResults.forEach { result ->
                    ListItem(
                        headlineContent = { Text(result) },
                        modifier = Modifier
                            .clickable {
                                textFieldState.edit { replace(0, length, result) }
                                expanded = false
                            }
                            .fillMaxWidth()
                    )
                }
            }
        }
    }
}

코드 관련 핵심 사항

  • rememberSaveable은 검색창이 펼쳐져 있는지 접혀 있는지 여부가 구성 변경 간에 유지되도록 합니다. 구성 변경 중에 Activity가 소멸되기 전에 저장된 값을 호스팅 Activity의 savedInstanceState 번들에 씁니다.
  • semantics 수정자는 TalkBack 탐색 순서를 제어합니다.
    • isTraversalGroupBox가 모든 하위 컴포저블을 그룹화하도록 설정됩니다.
    • traversalIndex는 TalkBack이 각 그룹 피어에서 접근성 정보를 읽는 순서를 지정하도록 설정됩니다. TalkBack은 1과 같은 양수 값을 가진 피어보다 -1과 같은 음수 값을 가진 피어의 접근성 정보를 먼저 읽습니다. 값은 부동 소수점 수이므로 각 피어에서 -1.01.0 사이의 값을 설정하여 여러 피어의 맞춤 순서를 지정할 수 있습니다.
  • SearchBar에는 사용자 입력용 inputField와 검색 추천을 표시하는 Column가 포함되어 있습니다.
    • SearchBarDefaults.InputField는 입력란을 만들고 사용자 쿼리의 변경사항을 처리합니다.
    • onQueryChange는 텍스트 입력을 처리하고 입력란의 텍스트가 변경될 때마다 상태를 업데이트합니다.
    • The expanded 상태는 추천 목록의 공개 상태를 제어합니다.
  • searchResults.forEach { result -> … }searchResults 목록을 반복하고 각 결과에 대해 ListItem를 만듭니다.
    • ListItem를 클릭하면 textFieldState가 업데이트되고 검색창이 접히며 선택한 검색 결과로 textField가 채워집니다.

결과

검색창이 표시되어 있으며 검색창 안에 &#39;a&#39; 문자가 입력되어 있습니다. 검색창 아래에 6개의 검색 추천이 포함된 목록이 표시됩니다.
그림 2. 추천 검색어가 표시된 검색창

필터링된 목록이 있는 검색창

이 예에서는 사용자의 검색어를 기반으로 목록을 필터링하는 SearchBar를 보여줍니다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomizableSearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    onSearch: (String) -> Unit,
    searchResults: List<String>,
    onResultClick: (String) -> Unit,
    // Customization options
    placeholder: @Composable () -> Unit = { Text("Search") },
    leadingIcon: @Composable (() -> Unit)? = { Icon(Icons.Default.Search, contentDescription = "Search") },
    trailingIcon: @Composable (() -> Unit)? = null,
    supportingContent: (@Composable (String) -> Unit)? = null,
    leadingContent: (@Composable () -> Unit)? = null,
    modifier: Modifier = Modifier
) {
    // Track expanded state of search bar
    var expanded by rememberSaveable { mutableStateOf(false) }

    Box(
        modifier
            .fillMaxSize()
            .semantics { isTraversalGroup = true }
    ) {
        SearchBar(
            modifier = Modifier
                .align(Alignment.TopCenter)
                .semantics { traversalIndex = 0f },
            inputField = {
                // Customizable input field implementation
                SearchBarDefaults.InputField(
                    query = query,
                    onQueryChange = onQueryChange,
                    onSearch = {
                        onSearch(query)
                        expanded = false
                    },
                    expanded = expanded,
                    onExpandedChange = { expanded = it },
                    placeholder = placeholder,
                    leadingIcon = leadingIcon,
                    trailingIcon = trailingIcon
                )
            },
            expanded = expanded,
            onExpandedChange = { expanded = it },
        ) {
            // Show search results in a lazy column for better performance
            LazyColumn {
                items(count = searchResults.size) { index ->
                    val resultText = searchResults[index]
                    ListItem(
                        headlineContent = { Text(resultText) },
                        supportingContent = supportingContent?.let { { it(resultText) } },
                        leadingContent = leadingContent,
                        colors = ListItemDefaults.colors(containerColor = Color.Transparent),
                        modifier = Modifier
                            .clickable {
                                onResultClick(resultText)
                                expanded = false
                            }
                            .fillMaxWidth()
                            .padding(horizontal = 16.dp, vertical = 4.dp)
                    )
                }
            }
        }
    }
}

코드 관련 핵심 사항

  • onQueryChange 람다 함수는 사용자가 검색창에 텍스트를 입력하거나 삭제할 때마다 호출됩니다.
  • SearchBarDefaults.InputField에는 입력란 시작 부분에 검색 아이콘을 추가하는 leadingIcon와 입력란 끝에 '옵션 더보기' 아이콘을 추가하는 trailingIcon가 포함되어 있습니다. 여기에서 사용자에게 정렬 및 필터링 옵션을 제공할 수 있습니다.
  • onSearch = { … }onSearch 람다를 호출하고 검색이 제출되면 검색창을 접습니다.
  • LazyColumn는 잠재적으로 많은 수의 검색 결과를 효율적으로 처리합니다. searchResults 목록을 반복하고 각 결과를 ListItem로 표시합니다.
  • ListItem 컴포저블은 상품 텍스트, 추가 정보를 보여주는 텍스트, 별표 아이콘을 상품의 leadingContent로 표시합니다. 이 예에서는 항목을 즐겨찾기에 추가하는 옵션이 표시됩니다.
  • 필터링 로직은 GitHub의 전체 소스 코드에서 CustomizableSearchBarExample를 참고하세요.

결과

텍스트 검색 힌트라는 단어가 포함된 검색창이 표시됩니다. 검색창 아래에 검색 추천 목록이 표시되며 각 추천 옆에 별표 아이콘이 표시됩니다.
그림 3. 관련 추천이 표시된 검색창

추가 리소스