Coroutine Flow debounce 를 이용한 검색 기능 구현

이지훈·2022년 3월 5일
0

앱 내에 검색 기능을 구현할 때, editText의 변화가 감지될때마다 api를 호출할 경우
너무 많은 비용이 소모될 수 있다. 단어가 완성되기 전에 몇번이고, 오타를 낼때마다 의도치 않게 api를 호출하고, 이것을 한명의 유저만 수행하는 것이 아니기때문에...

따라서 검색을 구현할때는 입력이 발생하는 즉시 가 아닌 어느정도 시간의 텀을 두고 유저의 입력이 종료되었다고 판단될 무렵에 api를 호출하는 것이 바람직하다.

기존의 RxJava 를 통해 구현했던 코드는 다음과 같다.

	// rx 적용부분
    // 옵저버블 통합 제거를 위한 CompositeDisposable
    private var myCompositeDisposable = CompositeDisposable()
    
            // 에딧텍스트 옵저버블
            val editTextChangeObservable = mySearchViewEditText.textChanges()

            val searchEditTextSubscription : Disposable =
                // 옵저버블에 오퍼레이터들 추가
                editTextChangeObservable
                    // 글자가 입력 되고 나서 0.8 초 후에 onNext 이벤트로 데이터 흘려보내기
                    .debounce(1000, TimeUnit.MILLISECONDS)
                    // IO 쓰레드에서 돌리겠다.
                    // Scheduler instance intended for IO-bound work.
                    // 네트워크 요청, 파일 읽기,쓰기, 디비처리 등
                    .subscribeOn(Schedulers.io())
                    // 구독을 통해 이벤트 응답 받기
                    .subscribeBy(
                        onNext = {
                            Log.d("RX", "onNext : $it")
                            //TODO:: 흘러들어온 이벤트 데이터로 api 호출
                            if (it.isNotEmpty()){
                                searchPhotoApiCall(it.toString())
                            }
                        },
                        onComplete = {
                            Log.d("RX", "onComplete")
                        },
                        onError = {
                            Log.d("RX", "onError : $it")
                        }
                    )
            // compositeDisposable 에 추가
            myCompositeDisposable.add(searchEditTextSubscription)
            
            
            
   	override fun onDestroy() {
        Log.d(TAG, "PhotoCollectionActivity - onDestroy() called")
        // 모두 삭제
        this.myCompositeDisposable.clear()

        super.onDestroy()
    }

Coroutine의 Flow 를 통한 구현

Extensions.kt

// 에딧텍스트 텍스트 변경을 flow 로 받기 
fun EditText.textChangesToFlow(): Flow<CharSequence?> {
	// flow 콜백 받기 
    return callbackFlow {
        val listener = object : TextWatcher {
            override fun afterTextChanged(s: Editable?) = Unit
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) =
                Unit

            override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) {
            	Log.d(TAG, "textChangesToFlow() / TextWatcher onTextChanged text : $text")
                //offer(text) is deprecated
                // 값 내보내기
                trySend(text)
            }
        }
        //위에서 설정한 리스너 달아주기 
        addTextChangedListener(listener)
        // 콜백이 사라질때 실행, 리스너 제거
        awaitClose { 
        	Log.d(TAG, "textChangesToFlow() awaitClose 실행")
        	removeTextChangedListener(listener) 
        }
    }.onStart {
    	// Rx의 onNext 와 동일 
        // 콜백이 시작될때 event를 방출
        Log.d(TAG, "textChangesToFlow() / onStart 발동")
        emit(text)
    }
}
 private var myCoroutineJob: Job = Job()
    private val myCoroutineContext: CoroutineContext
        get() = Dispatchers.IO + myCoroutineJob
        

			// Rx의 스케줄러와 비슷
            // IO 스레드에서 돌리겠다
            GlobalScope.launch(context = myCoroutineContext) {

                // editText 가 변경되었을때
                val editTextFlow = mySearchViewEditText.textChangesToFlow()

                editTextFlow
                    // 연산자들
                    // 입력되고 나서 1초 뒤에 받는다
                    .debounce(1000)
                    .filter {
                        it?.length!! > 0
                    }
                    .onEach {
                        Log.d(TAG, "flow로 받는다 $it")
                        // 해당 검색어로 api 호출
                        searchPhotoApiCall(it.toString())
                    }
                    .launchIn(this)
            }
            

  override fun onDestroy() {
        Log.d(TAG, "PhotoCollectionActivity - onDestroy() called")
        
        myCoroutineContext.cancel()

        super.onDestroy()
    }
            

collect 가 아닌 launchIn을 통해서 이벤트를 처리한다
그 이유는 launchIn은 collect와 다르게 새로운 코루틴을 생성하기 때문에
별도의 스레드, 별도의 코루틴에서 이벤트를 감시하고 처리할 수 있다.

새로운 스레드를 생성하지 않는 collect를 통해 event를 처리하면, event 는 버튼을 클릭하거나, 키보드 입력이 발생하는 등 페이지를 나가거나, 심지어 앱이 실행될 동안 계속 들어올 수 있기 때문에 다음에 발생할 동작(UI 갱신, Network 호출)들을 수행할 수 없다.

참고로 launchIn의 파라미터인 this 는 코루틴 스코프 (현재 Global scope 내부에 있기 때문에 해당 코루틴을 지칭한다)

이처럼 코루틴의 flow 를 이용하여 rxJava에서 수행하였던 debounce 를 비슷하게 구현할 수 있다.

reference)

https://stackoverflow.com/questions/63426845/android-edittext-coroutine-debounce-operator-like-rxjava

https://youtu.be/7m5T10OYGUA

https://peaku.co/questions/3769-android-edittext-coroutine-operador-de-rebote-como-rxjava

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글