본문으로 바로가기
반응형

Reactive Android UI

안드로이드에서 기본적으로 발생하는 이벤트들은 각 view에서 제공하는 event handler를 사용하여 처리합니다.
가장 기본적으로 onClickListener를 만들어 view의 click을 감지하고, EditTextView의 글자 변경을 TextWatcher를 이용해서 감지합니다.
 
하지만 이런 코드들은 상당히 boilerplate 합니다.
이런 이벤트들을 Reactive 하게 변경 함으로써 좀더 simple한 코드사용이 가능하며, 더불어 Rx의 장점을 이용할수 있습니다.
 
여기서 Reactive의 장점이라 함은 그 자체의 장점인 callback에서의 해방을 의미하기도 하며, Observable의 다양한 operator를 사용할수 있음을 의미합니다.
 
이렇게 View의 각 이벤트를 Reactive한 형태로 변경하기 위해서 RxBinding 라이브러리가 존재합니다.
사실 그냥 Observable의 생성하는 Listener를 직접 코드로 만들어도 되지만 이미 well-form하게 만들어진 library가 존재하기에 library를 이용하는게 좀 더 효율적 입니다. 

이 글은 하기 링크를 참고하였습니다.

https://code.tutsplus.com/tutorials/rxjava-for-android-apps-introducing-rxbinding-and-rxlifecycle--cms-28565

https://academy.realm.io/posts/donn-felker-reactive-android-ui-programming-with-rxbinding/

https://academy.realm.io/kr/posts/compose-everything-rx-kotlin/

Import

gradle 기준 아래 library를 추가하여야 합니다.
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
물론 최신 버전을 사용하도록 하며, 하기 링크에서 최신버전을 확인할 수 있습니다.
https://github.com/JakeWharton/RxBinding
 

해당 library를 import하면 기본적인 android ui component들에 대한 rxbinding을 수행할 수 있습니다.

만약 추가적인 androidX의 library에 대한 rxbinding의 지원이 필요하다면 아래와 같이 라이브러리를 추가하면 됩니다.

 

implementation 'com.jakewharton.rxbinding3:rxbinding-core:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-drawerlayout:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-leanback:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-recyclerview:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-slidingpanelayout:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-swiperefreshlayout:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-viewpager:3.1.0'
implementation 'com.jakewharton.rxbinding3:rxbinding-viewpager2:3.1.0'
//Google /material bindings: 
implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.1.0'

 

기존에 사용하던 com.androidX.xxxx를 위와 같은 형태로 (동일한 prefix)를 사용하여 손쉽게 추가할 수 있습니다.

 

또한 rxbinding3 이전 버전인 rxbinding2에서는 kotlin 지원을 위한 라이브러리가 따로 존재 했었습니다.

ex) implementation 'com.jakewharton.rxbinding3:rxbinding-kotlin:2.0.0'

하지만 rxbinding3부터는 'xxx-kotlin'으로는 따로 지원하지않고 위와 같이 추가만 하더라도, kotlin에서 rxbinding api들을 extention function 형태로 사용할 수 있습니다.

자세한 사용법은 아래 예제에서 설명합니다.

 

또한 Maven의 경우 아래사이트에서 최신 버전을 확인할 수 있습니다.

https://mvnrepository.com/artifact/com.jakewharton.rxbinding3

 

UI Event Binding

onClicks
 
가장 많이 사용하는 UI 이벤트로 onClick이 있습니다.
특정 view가 클릭되면 특정 동작을 하도록 하려면 ClickListener를 구현하여 view.setOnClickListener로 등록해줘야 하죠~
하지만 rxBinding을 사용하면 아래와 같이 등록할 수 있습니다.
 
Kotlin의 경우 View 객체에 바로 clicks() 함수를 추가하여 사용하여 Observable로 만들 수 있습니다.
 
fun processClickEvent(view: View) {
    val observable = view.clicks()
    observable.subscribe {
        Toast.makeText(context, "clicked!!", Toast.LENGTH_SHORT).show() 
    }
}

 

Java의 경우라면 아래와 같습니다.

 

public void processClickEvent(View view) {
    Observable ob = RxView.clicks(view);
    ob.subscribe(() -> Toast.makeText(context, "clicked!!", Toast.LENGTH_SHORT).show());
}

 

click이 되면 Toast를 띄웁니다.

실제로 Rxbinding source를 보면 아래와 같습니다.

 

/**
 * Create an observable which emits on `view` click events. The emitted value is
 * unspecified and should only be used as notification.
 * Warning:
 * The created observable keeps a strong reference to `view`. Unsubscribe
 * to free this reference.
 * Warning:* The created observable uses [View.setOnClickListener] to observe
 * clicks. Only one observable can be used for a view at a time.
 */
@CheckResult
fun View.clicks(): Observable {
    return ViewClickObservable(this)
}

private class ViewClickObservable(private val view: View) : Observable() {
    override fun subscribeActual(observer: Observer) {
        if (!checkMainThread(observer)) {
            return
        }
        val listener = Listener(view, observer) observer . onSubscribe (listener) view . setOnClickListener (listener)
    }

    private class Listener(private val view: View, private val observer: Observer) : MainThreadDisposable(), OnClickListener {
        override fun onClick(v: View) {
            if (!isDisposed) {
                observer.onNext(Unit)
            }
        }

        override fun onDispose() {
            view.setOnClickListener(null)
        }
    }
}

 

  • observer와 clickListener를 만듭니다.
  • 생성한 ClickListener를 View에 등록 합니다.(setOnClickListener)
  • clickListener의 onClick() 함수에서 observeronNext를 호출하면서 data를 방출시킵니다.
 
실제로 library 내부에서 click listener를 만들고 이를 view에 등록시키는 작업을 대신해주고 있습니다.
 
 
TextWatcher

많이 사용하는 기능중 EditTextTextWatcher를 이용하는 경우 아래와 같이 사용하면 됩니다.

 

fun processTextWatcher(tv: TextView) {
    val observable = tv.textChanges()
    observable.subscribe {
        charSequcne -> Toast.makeText(context, charSequcne.toString(), Toast.LENGTH_SHORT) 
    }
}

 

Java 라면 아래와 같습니다.

 

public void processTextWatcher(TextView tv) {
        Observable ob = RxTextView.textChanges(tv);
        ob.subscribe(charSequence -> Toast.makeText(context, charSequence.toString(), Toast.LENGTH_SHORT).show());
    }

 


 

실제 library 내부 코드는 아래와 같습니다.

 

/**
 * Create an observable of character sequences for text changes on `view`.
 * Warning:
 * Values emitted by this observable are **mutable** and owned by the host
 * `TextView` and thus are **not safe** to cache or delay reading (such as by observing
 * on a different thread). If you want to cache or delay reading the items emitted then you must
 * map values through a function which calls [String.valueOf] or
 * [.toString()][CharSequence.toString] to create a copy.
 * Warning:
 * The created observable keeps a strong reference to `view`. Unsubscribe
 * to free this reference.
 * Note:
 * A value will be emitted immediately on subscribe.
 */
@CheckResult
fun TextView.textChanges(): InitialValueObservable {
    return TextViewTextChangesObservable(this)
}

private class TextViewTextChangesObservable(private val view: TextView) : InitialValueObservable() {
    override fun subscribeListener(observer: Observer) {
        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) : 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)
        }
    }
}

 

Click Listener와 동일하게 TextWatcher 또한 library에서 대신 생성하여 EditTextView에 등록합니다.

또한 onTextChanged()가 호출되면 observeronNext()를 호출해 줍니다.

 

 

textChanges()의 경우 onTextChanged()에서 호출하나, 실제로 beforeTextChanged() 또는 afterTextChanged()에서 특정 코드작업이 필요할수도 있습니다.
따라서 아래와 같이 여러 api를 같이 제공합니다.

RxTextView의 확장함수로 다양한 기능을 제공하는 api가 제공됨을 알수 있습니다.

 

이렇게 UI event를 Observable로 변경함에 따라 단일 이벤트가 data stream의 출력으로 변경됩니다.

바꿔 얘기하면 UI event의 trigger를 이용해서 data stream의 emission을 발생시키고,  단순한 Toast 출력이나 UI 업데이트 뿐만 아니라 방대한 Rx의 operator를 통하여 복잡한 작업까지도 처리할 수 있습니다.

물론 콜백없이 말이죠~

 

Useful Operators

debounce를 이용한 SingleClcik (동일 view 연속 클릭 방지)

특히나 Android UI 처리를 위해서 유용하게 사용 가능한 Operator들의 예시는 아래와 같습니다.

 

button.clicks()
    .debounce(500, TimeUnit.MILLISECONDS)
    .subscribe { Toast.makeText(context, "Single Click", Toast.LENGTH_SHORT).show() }

위와 같이 onClick 이벤트를 처리하면 빠르게 여러번 tab시 한번만 동작하도록 막을 수 있습니다.

(debounce operator에 대한 예제는 아래 링크에서 확인하세요)

2019/12/16 - [개발이야기/Kotlin] - [RxKotlin] Reactive 코틀린 #5 - Processor, sample, throttle, window, buffer

View에 singleClick()이란 이름으로 Extension function으로 만들어 놓는다면 편리하겠죠?

 

merge 와 PublicSubject를 이용한 click(여러개의 view 동시 클릭 처리)

Android UI에서 여러 View를 동시에 누를경우 이슈가 발생하는 경우가 있습니다.

따라서 이런 경우 click 동작이 순차적으로 일어날 수 있도록 하나의 clickListener를 만들어 view가 공유해서 사용합니다.

물론 ClickListener 내부에서 onClick에서 view를 인자로 넘겨주므로 어떤 view의 click이 발생했는지 구분 가능하며, 이를 통해 클릭 동작을 분기해서 처리합니다.

Rxbinding의 경우 이벤트 발생시 각각의 observable을 생성하므로 여러 view의 동시 클릭을 순차적으로 처리하기 위해서는 어떻게 해야할지 알아봅니다.

 

case #1 - merge() operator를 이용

버튼을 세개 만들고 클릭시 반환되는 Observable을 merge operator를 이용하여 병합합니다.

 

class ClickActivity : Activity() {
    private val btn1 = Button(this) private
    val btn2 = Button(this) private
    val btn3 = Button(this) private
    val disposables = CompositeDisposable()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val btn1Ob = btn1.clicks().map { btn1 }
        val btn2Ob = btn2.clicks().map { btn2 }
        val btn3Ob = btn3.clicks().map { btn3 }
        val disposable = Observable.mergeArray(btn1Ob, btn2Ob, btn3Ob).subscribe {
            when (it) {
                btn1 -> { // Do something when btn1 clicked
                }
                btn2 -> { // Do something when btn1 clicked   
                }
                btn3 -> { // Do something when btn1 clicked
                }
                else -> {}
            }
        } disposables.add (disposable)
    }
    override fun onDestroy() {
        super.onDestroy() disposables . clear ()
    }
}

여기서는 observable이 세개이므로 mergeArray대신에 merge를  사용해도 됩니다.

또한 mergeArray로 생성된 Observable의 subscirber를 해제할 경우 병합된 observable 또한 같이 해제되어 메모리 이슈는 없습니다.

(메모리 이슈는 아래에 따로 언급했습니다.)

 

간단하게 Click을 순차적으로 할수 있도록 구성하였으나, 이는 리스너의 정의를 한곳에서 한 경우에만 사용 가능합니다.

즉, 버튼 두개만 onCreate()에서 하고 나머지 하나는 다른곳에서 하려고 한다면, 사용이 좀 애매해 집니다.

merge로 생성된 observable에 또 다른 observable을 merge를 해야 하며, 이럴 경우 새로운 observable이 생성되므로 subscribe도 세개가 머지된 observable에 다시 등록해 줘야 합니다.

이를 위해서 merge 된 observable을 클래스의 멤버변수로 가지고 있으면서 listener를 추가할 때 마다 새로운 observable로 바꿔줘야 하며, subscriber에 등록될 observer도 따로 가지고 있다가 등록해줘야 합니다.

구현은 할수 있으나, 깔끔하지가 않은거죠..

 

따라서 listener의 등록 위치가 분리되어 있을경우 아래와 같이 처리합니다.

case #2 -

PublicSubject

를 이용

 

class ClickActivity : Activity() {
    private val btn1 = Button(this) private
    val btn2 = Button(this) private
    val btn3 = Button(this) private
    val disposables = CompositeDisposable()
    val listenerObservable = PublishSubject.create<View>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val disposableListener = listenerObservable.subscribeOn(AndroidSchedulers.mainThread()).subscribe {
            when (it) {
                btn1 -> { // Do something when btn1 clicked 
                }
                btn2 -> { // Do something when btn1 clicked            
                }
                btn3 -> { // Do something when btn1 clicked                 
                }
                else -> {}
            }
        }
        val btn1Disposable = btn1.clicks().subscribe { listenerObservable.onNext(btn1) }
        val btn2Disposable = btn2.clicks().subscribe { listenerObservable.onNext(btn2) }
        val btn3Disposable = btn3.clicks().subscribe { listenerObservable.onNext(btn3) }
        disposables.addAll (btn1Disposable, btn2Disposable, btn3Disposable, disposableListener)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        disposables.clear()
    }
}

 


 

PublicSubject를 하나 선언하고 button 클릭을 처리할 내용을 subscribe로 등록합니다.

이때 subscribe는 UI thread에서 동작하도록 Scheduler를 지정합니다.

각 버튼이 클릭되면 PublicSubject로 이벤트를 전달하여 순차적으로 수행됩니다.

 

이렇게 subject를 이용하면 중간 어디에서든 새로운 click listener를 추가할 수 있습니다.

 

 

 

Hot Observable을 이용한 Multiple Listener 등록

보통 Android에서는 다수의 event listener의 등록을 제공하지 않습니다.

(위에 언급한 click Listener는 setOnClickListener를 이용해서 하나의 listener만 등록할수 있습니다. -> 물론 TextWatcher는 add 함수를 이용하여 여러개 등록이 가능합니다)

하지만 Observable을 이용하여 다수의 Listener를 등록할 수 있으며, 이때 Observable에 publish()를 이용하여 Hot Observable로 변경하여 사용합니다.

publish()를 사용하면 ObservableConnectedObservable로 변경되고, subscribe가 이루어 지더라도 바로 데이터를 방출하지 않습니다.

 

따라서 여러개의 Observer의 등록이 필요한 경우, 모두 subscriber로 등록후에 connect()함수를 호출함으로써 모든 listener에 동일한 event를 전달할면서 listening을 시작할 수 있습니다.

Hot Observable에 대한 내용은 아래 링크를 확인하세요.

2019/12/04 - [개발이야기/Kotlin] - [RxKotlin] Reactive 코틀린 #2 - Observable

 

그외 유용한 tips

https://academy.realm.io/kr/posts/compose-everything-rx-kotlin/ 링크를 확인하면 android에서 좀더 다양한 rxjava 및 rxbinding 활용 예제를 확인할 수 있습니다.
 
위 링크에서는 버튼을 클릭하면 progress bar를 띄우고 네트워크에 접속하여 결과를 받아와서 화면에 그리는 작업을 매우 짧은 하나의 observable을 이용해서 구현한 내용을 확인할 수 있습니다~~

 

Android memory leaks

RxJava를 이용하면 간결한 코드로 좀더 reactive 하고 interactive한 Android application을 만들 수 있습니다.
물론 위에 언급한 기능들이 RxJava없이도 충분히 구현 가능하지만, RxJava의 operator를 이용함으로써 좀더 simple하게 기능을 구현할 수 있습니다.
 
하지만 Activity가 running하는 Observable을 가지고 있다면, destory 되더라도 해당 Observer가 activity의 reference를 가지게 되기 때문에 memory leak이 발생합니다.
(Activity가 Observer에 의해 strong reference로 걸려있으니  GC가 garbage collection을 못하게 되는거죠..)
 
방법1. RxLifeCycle library의 이용.

 

 

Activiy나 Fragment의 life cycle에 따라서 Observable를 해제하는 기능을 가진 RxLifeCycle library가 존재합니다.

최신 버전은 하기 git link나 maven repository 링크에서 확인 가능합니다. (현재 최신 버전은 3.1.0 이네요.)

https://mvnrepository.com/artifact/com.trello.rxlifecycle3/rxlifecycle

https://github.com/trello/RxLifecycle

 

해당 버전을 gradle이나 maven에 추가하여 import할 수 있습니다.

 

먼저 이 라이브러리 사용을 위해서는 RxActivity, RxAppCompatActivity 또는 RxFragment를 기본 Activity나 Fragment 대신에 사용해야 합니다.

 

import com.trello.rxlifecycle2.components.support.RxAppCompatActivity

class MyActivity : RxAppCompatActivity() { ... }

기본적으로 Observable 등록시 상호 보안적인 life cycle에서 해제하도록 하려면 RxLifecycleAndroid.bindActivity 를 이용하여 Observable을 등록합니다.

 

 

 

val ob = Observable.range(1, 10) ... override
fun onCreate() {
    ...
    ob.compose(RxLifecycleAndroid.bindActivity(lifecycle)).subscribe { ... }
}

 

위와 같이 설정하면 lifecycle의 상호 이벤트인 onDestroy()에서 해당 Observable이 해제 됩니다.

반대로 Fragment의 onAttach()에서 등록된 Observable은 onDetach()에서 해제 됩니다.

 

만약 특정 이벤트에서 해제가 필요하다면 bindUntilEvent를 이용하여 명시적으로 해당 이벤트를 줄수도 있습니다.

 

val ob = Observable.range(1, 10) ... override
fun onCreate() {
    ...
    ob.compose(RxLifecycleAndroid.bindUntilEvent(lifecycle, ActivityEvent.DESTROY)).subscribe { ... }
}

 

라이브러리를 사용할 경우의 이점도 있지만, 해당 library를 import하는 부담과 정해진 RxActivity나 RxFragment를 상속받아 써야한다는 부담이 존재합니다.

또한 어차피 생성시점에 compose로 명시해야 한다면 그 역시 휴먼에러로 누락 위험성이 있어 보입니다.

(소개는 했지만 저는 안쓰는걸로....)

 

방법2. CompositeDisposable 의 이용

CompositeDisposable은 disposable을 담아두는 container로 한번에 해제하는 기능을 제공합니다.

subscribe 함수 호출시 반환되는 disposable을 CompositeDisposable 객체에 담아두고 필요한 시점에 한번에 해제 시킵니다.

 

class MyActivity : Activity() {
    val disposables = CompositeDisposable()
    ....
    override fun onCreate() {
        val disposable1 = Observable.range(1, 10).subscribe { ... }
        disposables.add(disposable1)         
        ....
    }

    override fun onResume() {
        val disposable2 = Observable.range(1, 10).subscribe { ... }
        disposables.add(disposable1)
        ....
    }

    override fun onStop() {
        disposables.clear()
        ....
    }
    ....
}

 

CompositeDisposable.add() 함수를 통해 disposable을 등록시키고 CompositeDisposable.clear() 로 등록된 subscriber를 한번에 해제 시킬수 있습니다.

그외에 remove(), delete()등의 함수와 생성자로 disposable을 넘겨받을수 있으니, 자세한 부분은 해당 class를 열어 주석을 참고하시기 바랍니다.

 

 

반응형