앱 내에 검색 기능을 구현할 때, 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://peaku.co/questions/3769-android-edittext-coroutine-operador-de-rebote-como-rxjava