RxJava 라이브러리를 통해 검색 기능을 구현하는 부분에 대해 다뤄보겠습니다.
RxJava를 통해 검색 중에 Reactive Steam으로 사용자가 입력 중인 검색어에 따라 debounce로 실시간 검색 처리가 가능합니다.
먼저 RxJava에 대한 개념 몇 가지를 살펴보자면..
- Reactive Programming: 데이터를 비동기 스트림으로 처리하는 프로그래밍 패러다임입니다. 데이터의 흐름을 관찰하면서 이벤트가 발생하면 이를 구독자에게 전달하여 처리하는 방식입니다.
- Observable: 스트림에서 발생하는 이벤트나 데이터를 발행하는 주체입니다. TextView의 텍스트 변화처럼 연속적인 이벤트를 관찰하고, 이에 대해 구독자에게 이벤트를 전달합니다.
- Observer: Observable이 발행하는 이벤트나 데이터를 구독하고, 이를 처리하는 주체입니다.
- Disposable: 구독을 취소하는 메커니즘입니다.
- Schedulers: RxJava에서는 작업이 실행될 스레드를 설정할 수 있습니다. 예를 들어, UI 작업은 메인 스레드에서, 네트워크 요청은 I/O 스레드에서 실행되도록 처리할 수 있습니다.
검색 기능의 중추가 되는 EditText 컴포넌트는 사실 Observer 패턴에 매우 적합하다고 볼 수 있습니다. 값을 입력하고 해당 값에 따라 개발자는 값을 처리하는 로직을 구성, 사용자는 데이터를 발행받음으로써 이 행위 자체를 Observable로 생각하면 활용성을 높일 수 있겠죠.
사실 TextWatcher로 입력값을 변화해서 매번 처리하는 방법도 있겠으나.. 불필요한 콜백 함수의 추가와 debounce 등의 메서드를 활용한 간결한 처리가 가능하기 때문에 Rx를 적용하기로 결정했습니다.
여기서 debounce는 RX 한정적인 단어는 아니고, 짧은 시간 동안 반복적으로 발생하는 이벤트를 제어하는 기법으로 이해해주시면 되겠습니다.
직접 Rx를 구현하는 것도 좋지만 라이브러리를 사용하여 검색 기능을 구성해 보겠습니다.
https://github.com/JakeWharton/RxBinding
GitHub - JakeWharton/RxBinding: RxJava binding APIs for Android's UI widgets.
RxJava binding APIs for Android's UI widgets. Contribute to JakeWharton/RxBinding development by creating an account on GitHub.
github.com
implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0'
라이브러리 적용 이후, 활용할 곳에서
private var searchDisposable: Disposable? = null
disposable을 먼저 선언해 줍니다.
disposable은 구독을 관리하고, 해제하는 역할을 담당합니다.
Rx를 처리하는 화면에서 나가거나 불필요해질 때 disposable을 통해 구독을 해제하고 리소스 및 메모리 낭비를 방지할 수 있습니다.
searchDisposable = it.searchEdit.textChanges()
.debounce(10, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = { char ->
if (char.isNotEmpty()) {
it.inputCancelBtn.visibility = View.VISIBLE
} else {
it.inputCancelBtn.visibility = View.GONE
}
//TODO: 입력 텍스트 기반 쿼리 동작
}
)
구성한 로직은 searchEditText에 textChanges()로 Observable 하도록 설정해 줍니다.
위 코드는 10ms 마다 이벤트를 수신받고 그 결과물에 따라 매번 여러 동작을 지정할 수 있는 로직입니다.
AndroidSchedulers의 경우에는 ui 작업이므로 Main Thread로 지정했습니다.
@CheckResult
fun TextView.textChanges(): InitialValueObservable<CharSequence> {
return TextViewTextChangesObservable(this)
}
private class TextViewTextChangesObservable(
private val view: TextView
) : InitialValueObservable<CharSequence>() {
override fun subscribeListener(observer: Observer<in CharSequence>) {
val listener = Listener(view, observer)
observer.onSubscribe(listener)
view.addTextChangedListener(listener)
}
override val initialValue get() = view.text
private class Listener(
private val view: TextView,
private val observer: Observer<in CharSequence>
) : MainThreadDisposable(), TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (!isDisposed) {
observer.onNext(s)
}
}
override fun afterTextChanged(s: Editable) {
}
override fun onDispose() {
view.removeTextChangedListener(this)
}
}
}
라이브러리에서 제공하는 textChanges() 위와 같이 Char 구독이 가능한 Observable 형태를 반환하는데,
구독 리스너가 TextWatcher를 통해 변화값을 전달할 수 있는 것으로 구성됩니다.
해당 라이브러리를 사용함으로써 저희가 직접 구성해 줄 필요가 없습니다.
@CheckReturnValue
@SchedulerSupport(SchedulerSupport.NONE)
fun <T : Any> Observable<T>.subscribeBy(
onError: (Throwable) -> Unit = onErrorStub,
onComplete: () -> Unit = onCompleteStub,
onNext: (T) -> Unit = onNextStub
): Disposable = subscribe(onNext.asConsumer(), onError.asOnErrorConsumer(), onComplete.asOnCompleteAction())
subscribeBy에는 3가지 콜백 함수가 있는데,
onError는 값을 방출하는 중에 Exception이 발생한 상황일 경우 처리이고, onComplete의 경우에는 더 이상 값이 방출할 것이 남아있지 않은 경우입니다.
단순 EditText 만으로는 해당 콜백들을 사용할 이유가 없다고 판단하여 매번 값이 방출될 때마다 호출되는 onNext만을 활용했습니다.