Published on

Implement chức năng search trong Android

Implement chức năng search trong Android

Search với API là tính năng xuất hiện ở hầu hết các ứng dụng Android hiện đại: tìm kiếm sản phẩm, user, tài liệu, video… Việc triển khai search tưởng đơn giản nhưng nếu làm không khéo, app sẽ spam API khi user gõ liên tục, giật lag UI, tốn pin và băng thông, dễ crash nếu lifecycle không được xử lý đúng.


Vấn đề thường gặp khi Search API

Giả sử user gõ: a, rồi ab, rồi abc.

Nếu bạn gọi API ngay mỗi lần text thay đổi:

  • App sẽ gửi 3 request → request đầu về sau request sau → UI hiển thị sai data
  • Lãng phí tài nguyên
  • Khó quản lý cancel

Giải pháp chuẩn:

  • debounce
  • cancel request cũ
  • chỉ gửi request mới nhất

ViewModel: sử dụng Job để debounce + cancel API cũ

Dưới đây là ViewModel xử lý toàn bộ logic search.

class SearchViewModel(
    private val repository: SearchRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(SearchUiState())
    val uiState = _uiState.asStateFlow()

    private var searchJob: Job? = null

    fun onQueryChange(query: String) {
        _uiState.update { it.copy(query = query) }

        searchJob?.cancel()

        searchJob = viewModelScope.launch {
            delay(350) // debounce 350ms

            if (query.isNotBlank()) {
                _uiState.update { it.copy(isLoading = true) }

                try {
                    val result = repository.search(query)
                    _uiState.update {
                        it.copy(
                            results = result,
                            isLoading = false,
                            error = null
                        )
                    }
                } catch (e: Exception) {
                    _uiState.update {
                        it.copy(
                            isLoading = false,
                            error = e.message ?: "Error"
                        )
                    }
                }
            } else {
                _uiState.update { it.copy(results = emptyList()) }
            }
        }
    }
}

data class SearchUiState(
    val query: String = "",
    val results: List<String> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

Tại sao dùng Job?

  • searchJob?.cancel() đảm bảo request cũ bị huỷ ngay.
  • Chỉ request mới nhất được chạy.
  • Không cần Flow phức tạp → code rõ ràng.

ViewModel: sử dụng Flow + debounce (tùy chọn)

class SearchViewModel(
    private val repository: SearchRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(SearchUiState())
    val uiState = _uiState.asStateFlow()

    init {
        _uiState
            .map { it.query }
            .debounce(350)
            .distinctUntilChanged()
            .flatMapLatest { keyword ->
                flow {
                    if (keyword.isBlank()) {
                        _uiState.update {
                            it.copy(
                                results = emptyList(),
                                isLoading = false,
                                error = null
                            )
                        }
                        return@flow
                    }

                    _uiState.update {
                        it.copy(
                            isLoading = true,
                            error = null
                        )
                    }

                    emit(repository.search(keyword))
                }
            }
            .onEach { data ->
                _uiState.update {
                    it.copy(
                        isLoading = false,
                        results = data,
                        error = null
                    )
                }
            }
            .catch { e ->
                _uiState.update {
                    it.copy(
                        isLoading = false,
                        error = e.message ?: "Error"
                    )
                }
            }
            .launchIn(viewModelScope)
    }

    fun onQueryChange(text: String) {
        _uiState.update {
            it.copy(query = text)
        }
    }
}

data class SearchUiState(
    val query: String = "",
    val isLoading: Boolean = false,
    val results: List<String> = emptyList(),
    val error: String? = null
)

UI với Jetpack Compose: SearchBar + List

Ví dụ UI đơn giản với TextField và LazyColumn.

  • Nhận input từ TextField
  • Hiển thị loading indicator
  • Hiển thị kết quả search
@Composable
fun SearchScreenContainer(viewModel: SearchViewModel = hiltViewModel()) {
    val state by viewModel.uiState.collectAsState()
    SearchScreen(state)
}

@Composable
fun SearchScreen(state: SearchUiState) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {

        TextField(
            value = state.query,
            onValueChange = viewModel::onQueryChange,
            modifier = Modifier.fillMaxWidth(),
            placeholder = { Text("Search...") }
        )

        Spacer(Modifier.height(12.dp))

        if (state.isLoading) {
            CircularProgressIndicator()
        }

        if (state.error != null) {
            Text("Error: ${state.error}", color = Color.Red)
        }

        LazyColumn {
            items(state.results) { item ->
                Text(
                    text = item,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                )
            }
        }
    }
}

Khi nào nên dùng Job? Khi nào nên dùng Flow?

Dùng Job nếu bạn chỉ làm:

  • Search đơn giản
  • Một input duy nhất
  • Không cần combine nhiều flows

→ Job dễ debug, dễ maintain hơn.

Dùng Flow nếu bạn cần:

  • Kết hợp nhiều nguồn dữ liệu: query, filter, sort, refresh
  • Có nhiều nguồn dữ liệu cần combine
  • App cần reactive, mở rộng dễ dàng

Kết luận

Cả hai cách trên đều hiệu quả để implement chức năng search trong Android. Chọn cách nào phụ thuộc vào độ phức tạp của yêu cầu và kiến trúc ứng dụng của bạn. Quan trọng nhất là đảm bảo trải nghiệm người dùng mượt mà, không lag, không spam API. Chúc bạn thành công!