[출처 - RxJava 프로그래밍 : 리액티브 프로그래밍 기초부터 안드로이드 까지 한번에]
본 글은 'RxJava 프로그래밍 : 리액티브 프로그래밍 기초부터 안드로이드 까지 한번에' 를 학습하면서 정리한 글입니다.
for문에 대한 처리를 한번 해보겠습니다.
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="btn_1" />
<Button
android:id="@+id/btn_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="btn_2"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity
package com.anapass.rxandroid_test
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import io.reactivex.Observable
import io.reactivex.observers.DisposableObserver
import io.reactivex.rxkotlin.toObservable
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
val TAG: String = "Test_RxAndroid"
val samples = listOf("banana","orange", "apple","apple mango","melon","watermelon")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_1.setOnClickListener {
Log.d( TAG, "btn_1 click")
for( s in samples){
if(s.contains("apple")){
Log.d(TAG, s)
}
}
}
btn_2.setOnClickListener {
Log.d( TAG, "btn_2 click")
samples.toObservable()
.filter { s-> s.contains("apple") }
.defaultIfEmpty("Not found")
// .first("Not found")
.subscribe { it-> Log.d(TAG,it) }
}
}
}
결과
2020-02-10 16:07:45.218 13224-13224/com.anapass.rxandroid_test D/Test_RxAndroid: btn_1 click
2020-02-10 16:07:45.220 13224-13224/com.anapass.rxandroid_test D/Test_RxAndroid: apple
2020-02-10 16:07:45.220 13224-13224/com.anapass.rxandroid_test D/Test_RxAndroid: apple mango
2020-02-10 16:07:46.097 13224-13224/com.anapass.rxandroid_test D/Test_RxAndroid: btn_2 click
2020-02-10 16:07:46.105 13224-13224/com.anapass.rxandroid_test D/Test_RxAndroid: apple
2020-02-10 16:07:46.105 13224-13224/com.anapass.rxandroid_test D/Test_RxAndroid: apple mango
이렇게 filter를 사용하여 for문을 작성할 수 있습니다. 이 외에도 다양한 문법에 대하여 RxJava, RxKotlin으로 작성할 수 있습니다. 아마 가장 유용한 부분은 Scheduler들이 아닐까 싶습니다.
우선 간단한 code를 확인하였고, RxLifecycle에 대해 알아보도록 하겠습니다.
RxLifecycle
RxLifecycle이란 android의 엑티비티와 프래그먼트의 라이프 사이클을 RxJava 혹은 RxKotlin에서 사용할 수 있게 도와주는 라이브러리 입니다. 즉, RxJava , RxKotlin을 Android app 개발에 사용하였을 때 발생하는 메모리 누수를 방지하기 위해 사용됩니다.
RxLifecycle을 이용하여 안드로이드의 라이프 사이클에 맞게 Observale을 관리할 수 있습니다. 관련 컴포넌트들은
RxActivity / RxDialogFragment / RxFragment / RxPreferenceFragment / RxAppCompatActivity / RxAppCompatDialogFragment / RxFragmentActivity 가 있습니다.
그럼 RxLifecycle을 사용한 간단한 code를 보겠습니다.
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/main_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity
package com.anapass.rxandroid_test
import android.os.Bundle
import com.trello.rxlifecycle3.components.support.RxAppCompatActivity
import io.reactivex.Observable
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : RxAppCompatActivity() {
val TAG: String = "Test_RxAndroid"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Observable.just("Hello RxLifecycle")
.compose(bindToLifecycle())
.subscribe { main_text.text = it }
}
override fun onDestroy() {
super.onDestroy()
}
}
이렇게 compose에 bindToLifecycle을 하면 onDestroy시 Observable이 자동으로 해제 됩니다. 이 방법 이외에도
Disposable을 사용하여 개발자가 직접 해제할 수 도 있습니다. 그 방법은 다음과 같습니다.
package com.anapass.rxandroid_test
import android.os.Bundle
import com.trello.rxlifecycle3.components.support.RxAppCompatActivity
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : RxAppCompatActivity() {
val TAG: String = "Test_RxAndroid"
var source : Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
source = Observable.just("Hello RxLifecycle")
.compose(bindToLifecycle())
.subscribe { main_text.text = it }
}
override fun onDestroy() {
super.onDestroy()
source?.dispose()
}
}
마지막으로 UI 이벤트 처리를 확인해보겠습니다.
UI 이벤트 처리
안드로이드는 사용자가 App과 상호작용( 버튼 클릭과 같은 액션)을 할 경우 특정 View 객체의 이벤트를 얻는 방법을 제공합니다. 이 말은 View 클래스 안에 UI이벤트 처리를 위한 몇가지 골백 메서드가 있다는 말과 같습니다.
이를 이벤트 리스너라 부르고 다음과 같은 콜백 메서드를 가집니다.
onClick() / onLongClick() / onFocusChange() / onKey() / onTouch() / onCreateContextMenu()
(각각의 자세한 설명은 android developer에서 확인 하실 수 있습니다.)
그럴 onClick에 Observable을 사용한 예제를 만들어 보겠습니다.
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/main_btn"
android:text="Main Btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity
package com.anapass.rxandroid_test
import android.os.Bundle
import android.view.View
import com.trello.rxlifecycle3.components.support.RxAppCompatActivity
import io.reactivex.Observable
import io.reactivex.ObservableEmitter
import io.reactivex.ObservableOnSubscribe
import io.reactivex.observers.DisposableObserver
import kotlinx.android.synthetic.main.activity_main.*
import android.util.Log
class MainActivity : RxAppCompatActivity() {
val TAG: String = "Test_RxAndroid"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getClickEventObservable()
.map {
s->"clicked"
}
.subscribe(getObserver())
}
fun getClickEventObservable(): Observable<View>{
return Observable.create(object : ObservableOnSubscribe<View> {
override fun subscribe(emitter: ObservableEmitter<View>) {
main_btn.setOnClickListener {
emitter.onNext(it)
}
}
})
}
private fun getObserver(): DisposableObserver<in String> {
return object : DisposableObserver<String>() {
override fun onNext(s: String) {
Log.d(TAG, s)
}
override fun onError(e: Throwable) {
Log.e(TAG,e.message)
}
override fun onComplete() {
Log.d(TAG,"complete")
}
}
}
}
결과
버튼을 누르면 Log에
2020-02-10 16:47:18.011 14689-14689/com.anapass.rxandroid_test D/Test_RxAndroid: clicked
결과가 나타납니다.
이 동작은 버튼을 누르면 setOnClickListenr() 메서드가 호출되고 거기서 emitter.onNext(it)으로 view를 전달하면 map 함수가 clicked라는 String르오 값을 변경합니다. 그리고 그 결과를 getObserver 함수가 반환하는 DiposableObserver의 onNext에서 Log로 보여줍니다.
더 간단하게는 다음과 같이 Activity를 수정하면 됩니다.
class MainActivity : RxAppCompatActivity() {
val TAG: String = "Test_RxAndroid"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getClickEventObservable()
.map {
s->"clicked"
}
.subscribe{it-> Log.d(TAG,it)}
}
fun getClickEventObservable(): Observable<View>{
return Observable.create(object : ObservableOnSubscribe<View> {
override fun subscribe(emitter: ObservableEmitter<View>) {
main_btn.setOnClickListener {
emitter.onNext(it)
}
}
})
}
그리고 RxView를 사용하는 방법도 있습니다.
RxView를 사용하기 위해서는 책에서는
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0"
을 사용하였지만 최신버전을 확인해보니 (확인은 여기서) 다음과 같아 다음을 추가하였습니다.
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
이렇게 추가를 하고 RxView를 쓸려고 하니 RxView가 인포트 되지 않아서 확인 해본 결과 작성하는 현재 rxbinding3에서는 RxView가 없는 것으로 확인되었습니다. 그래서 rxbinding2를 다시 추가하여 만든 코드는
package com.anapass.rxandroid_test
import android.os.Bundle
import android.util.Log
import com.jakewharton.rxbinding2.view.RxView
import com.trello.rxlifecycle3.components.support.RxAppCompatActivity
import io.reactivex.Observable
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : RxAppCompatActivity() {
val TAG: String = "Test_RxAndroid"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getClickEventObservable().subscribe{it-> Log.d(TAG,it)}
}
fun getClickEventObservable(): Observable<String>{
return RxView.clicks(main_btn)
.map { s->"Click binding" }
}
}
버튼을 눌린 결과는
2020-02-10 17:27:56.135 15438-15438/com.anapass.rxandroid_test D/Test_RxAndroid: Click binding
와 같이 나오고 있습니다.
아직 rxbinding3에서는 RxView를 지원하지 않는것으로 보이니 이에 대한 사용은 잠시 미루는게 좋을 것으로 보입니다.(rxbinding2가 2018 9월 이후로 따로 업데이트가 없어서 최신을 사용하는 것이 맞다고 생각하기 때문입니다.)
그리고 마지막으로 이에 대한 응용 code를 보면
package com.anapass.rxandroid_test
import android.os.Bundle
import android.util.Log
import android.view.View
import com.trello.rxlifecycle3.components.support.RxAppCompatActivity
import io.reactivex.Observable
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
class MainActivity : RxAppCompatActivity() {
val TAG: String = "Test_RxAndroid"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getClickEventObservable()
.map { num->7 }
.flatMap {it-> val random = Random()
val data = random.nextInt(10)
Observable.just("local : $it", "remote : $data", "result = " + (it == data))}
.subscribe { it->Log.d(TAG, it) }
}
fun getClickEventObservable(): Observable<View>{
return Observable.create<View> { s -> main_btn.setOnClickListener {s.onNext(it) } }
}
}
결과는
2020-02-10 17:36:41.057 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: local : 7
2020-02-10 17:36:41.057 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: remote : 7
2020-02-10 17:36:41.057 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: result = true
2020-02-10 17:36:42.047 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: local : 7
2020-02-10 17:36:42.048 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: remote : 4
2020-02-10 17:36:42.048 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: result = false
2020-02-10 17:36:42.900 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: local : 7
2020-02-10 17:36:42.900 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: remote : 9
2020-02-10 17:36:42.900 15729-15729/com.anapass.rxandroid_test D/Test_RxAndroid: result = false
위와 같이 7일 경우에는 true를 아닐 경우에는 false를 반환하는 code를 작성할 수 있습니다.
위 code는 버튼을 누를 경우 그 결과를 map을 통하여 7로 변경하고, flatMap으로 전달합니다. 그리고 flatMap에서 새로 만든 Observable에서 비교하여 3개의 결과를 반환합니다.(flatMap은 여기서 확인하시면 됩니다.)