ABOUT ME

-

  • Android) 코루틴 Channel과 Flow로 Instant Search 구현하기
    Android 2021. 2. 8. 22:11


    이번 글은 코루틴의 Flow Channel을 이용해 순간 검색을 구현해보는 글입니다.

     

    먼저 구현 화면을 첨부하겠습니다.

     

    저는 Room을 통해 저장된 아이템들을 불러오고, 해당 아이템을 검색했을 때의 결과를 화면에 표시했습니다.

     

    Instant Search with a flow

    첫번째 방법으로 flow & channel을 사용해 구현해 보겠습니다.

     

    ViewModel
     @ExperimentalCoroutinesApi
        val queryChannel = BroadcastChannel<String>(Channel.CONFLATED)
    
        @ExperimentalCoroutinesApi
        @FlowPreview
        val searchResult = queryChannel
            .asFlow()
            .debounce(300)
            .distinctUntilChanged() // do not want to collect the same value
            .flatMapLatest { query ->
                if (query.isBlank()) {
                    skinRepository.loadAllSkins()
                } else skinRepository.searchBySkinKinds(query)
            }
            .flowOn(dispatchers.default)
            .catch { e: Throwable ->
                e.printStackTrace()
            }
            .asLiveData()

     

    View
    binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?): Boolean {
                    return true
                }
    
                override fun onQueryTextChange(newText: String?): Boolean {
                    newText?.let { viewModel.queryChannel.offer(it) }
                    return true
                }
            })
    
            viewModel.searchResult.observe(viewLifecycleOwner, Observer {
                adapter.submitList(it)
            })

     

    채널을 사용하는 것은 UI와 Flow logic 사이에 다리 역할을 합니다. search logic을 BroadcastChannel<String>과 capacity로 Channel.CONFLATED를 선언했습니다. BroadcastChannel 문서에 보면 deprecated 될 예정이라고 하네요?..

     

    뷰모델에서 debounce 오퍼레이터로 해당 ms동안 새로운 텍스트 입력이 없을 시 결과를 호출하고, 업데이트 시 중복 아이템을 구분하기 위해 distinctUnitlChanged 오퍼레이터를 사용, 중첩되는 flow를 평평하게 해 주기 위해 flatMapLatest 오퍼레이터를 사용, 뷰의 생명주기를 핸들링하기 위해 asLiveData() 확장 함수를 사용합니다.

     

    뷰(프래그먼트)에서 LiveData옵저빙 하고 있고, SearchView를 통해 onQueryTextChange 메소드에서 텍스트가 업데이트될 시에 queryChannel로 보내주는 방식입니다.

     

    채널에서 사용할 수 있는 메소드는 offer()와 send()가 있는데 약간의 작동방식에 있어 차이가 있습니다.

     

    • offer() - 즉시 element를 채널에 동기적으로 전송
    • send() - element를 채널로 전송하되, 채널의 버퍼가 가득 차거나 존재하지 않는 경우 지연시킴

     

    이번에는 두 번째 방법으로 flow & stateFlow를 사용해 구현해 보겠습니다.

     

    ViewModel
     // search by stateFlow
        private val _searchQuery = MutableStateFlow("")
    
        fun setSearchQuery(query: String) {
            _searchQuery.value = query
        }
    
        @ExperimentalCoroutinesApi
        @FlowPreview
        val result = _searchQuery
            .debounce(300)
            .flatMapLatest {
                    query ->
                if (query.isBlank()) {
                    skinRepository.loadAllSkins()
                } else skinRepository.searchBySkinKinds(query)
            }
            .flowOn(dispatchers.default)
            .catch { e: Throwable ->
                e.printStackTrace()
            }

     

    View
    binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?): Boolean {
                    return true
                }
    
                override fun onQueryTextChange(newText: String?): Boolean {
                    viewModel.setSearchQuery(newText.toString())
                    return true
                }
            })
    
    
            lifecycleScope.launchWhenStarted {
                viewModel.result.collect { adapter.submitList(it) }
            }
    

     

    채널을 사용했을 때와 크게 달라진 점은 없습니다.

    StateFlow의 특성상 초기 값을 세팅해주고 마찬가지로 텍스트의 업데이트가 있을 시에 텍스트를 뷰모델로 보내줍니다.

    StateFlowLiveData와 달리 생명주기를 자동으로 핸들링하지 못하기에 뷰에서 lifecycleScope를 선언해 처리해줍니다.

     

    lifecycleScope의 사용이 번거롭다면 asLiveData()를 사용할 수 있습니다.

     

    ViewModel
     @ExperimentalCoroutinesApi
        @FlowPreview
        val result = _searchQuery
            .debounce(300)
            .flatMapLatest {
                    query ->
                if (query.isBlank()) {
                    skinRepository.loadAllSkins()
                } else skinRepository.searchBySkinKinds(query)
            }
            .flowOn(dispatchers.default)
            .catch { e: Throwable ->
                e.printStackTrace()
            }
            .asLiveData()

    위의 코드에서 asLiveData() 확장 함수만 붙여주면 되겠네요?

     

    View
    viewModel.result.observe(viewLifecycleOwner, Observer {
                adapter.submitList(it)
            })

     

    그리고 뷰에서는 LiveData로서 옵저빙을 하는 게 가능해집니다.


    References

    Instant Search with Kotlin Coroutines

    반응형

    댓글

Designed by Me.