검색창

검색창을 사용하여 검색 기능을 구현합니다. 검색창은 사용자가 키워드나 문구를 입력하여 앱 내에서 관련 결과를 표시할 수 있는 지속적인 검색 필드이며, 검색이 앱의 기본 초점인 경우에 권장됩니다.

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

API 노출 영역

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

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

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

결과

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

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

다음은 사용자의 검색어에 따라 목록을 필터링하는 SearchBar의 예입니다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomizableSearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    onSearch: (String) -> Unit,
    searchResults: List<String>,
    onResultClick: (String) -> Unit,
    modifier: Modifier = Modifier,
    // 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,
) {
    // 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. 관련 추천이 표시된 검색창

추가 리소스