์๋ ํ์ธ์ ๐
์ค๋์ ์ ๊ฐ ํ๋ก์ ํธ๋ฅผ ํ๋ฉฐ ์ฌ์ฉํ๋ RxBinding์ ๋ํด์
ํฌ์คํ ํด๋ณด๋ ค ํฉ๋๋ค.

๋จผ์
RxBinding์ด๋?
RxBinding์ RxJava์ RxAndroid๋ฅผ ์ด์ฉํด
์๋๋ก์ด๋์ ์์ ฏ์ด๋ View์ Rx๋ฅผ ์ฌ์ฉํ๊ธฐ ์ฝ๊ฒ ํด์ฃผ๋ ์คํ์์ค์ ๋๋ค.
๋ํ์ ์ผ๋ก TextWatcher์ ๊ธฐ๋ฅ์ด๋ Button Click ์ด๋ฒคํธ ๋ฑ์ด ์์ต๋๋ค.
Rxbinding์ ํตํด ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ Operator๋ค๋ก๋
throttleFirst() , debounce()๊ฐ ์์ต๋๋ค.
์ด ๋ ํจ์์ ์ฐจ์ด์ ์ ๋ํด ์ฐ์ ์ค๋ช ๋๋ฆฌ๊ฒ ์ต๋๋ค.
throttleFirst()
์ ํด์ง ์๊ฐ ์์ ์ผ์ด๋๋ ์ฌ๋ฌ ์ด๋ฒคํธ๋ค์ ๊ฐ์ฅ ๋จผ์ ํธ์ถ๋ ์ด๋ฒคํธ๋ง ๋ฐฉ์ถํ๊ณ ๊ทธ ์ดํ์ ๋ค์ด์จ ์ด๋ฒคํธ๋ ๋ฌด์ํ๋ Operator์ ๋๋ค.
ํด๋น Operator๋ฅผ ํตํด ์ด์ค ํด๋ฆญ์ ๋ฐฉ์งํ๋ ๊ธฐ๋ฅ์ ๋ง์ด ๊ตฌํํฉ๋๋ค.
์๋๋ throttleFirst()๋ฅผ ํํํ Marble Diagram์ ๋๋ค.
debounce()
์ด๋ฒคํธ๊ฐ ํธ์ถ๋๊ณ ์ ํด์ง ์๊ฐ์ด ์ง๋ ํ์ ํด๋น ์ด๋ฒคํธ๋ฅผ ๋ฐฉ์ถํ๋ Operator์ ๋๋ค.
์๋ Marble Diagram์ ๋ณด๋ฉฐ ์ค๋ช ๋๋ฆฌ๊ฒ ์ต๋๋ค.
์ ๊ทธ๋ฆผ์ ๋ณด์๋ฉด ๋นจ๊ฐ ์ด๋ฒคํธ๊ฐ ํธ์ถ๋๊ณ ์ ํด์ง ์๊ฐ์ด ์ง๋ ํ์ ๋นจ๊ฐ ์ด๋ฒคํธ๊ฐ ๋ฐฉ์ถ๋๋ฉฐ
๋ ธ๋์๊ณผ ๋ น์ ์ด๋ฒคํธ๊ฐ ํธ์ถ๋์์ง๋ง, ๋ง์ง๋ง์ ํธ์ถ๋ ๋ น์ ์ด๋ฒคํธ๊ฐ ํธ์ถ๋ ํ ์ ํด์ง ์๊ฐ์ด ์ง๋ ํ์
๋ น์ ์ด๋ฒคํธ๋ง ๋ฐฉ์ถ๋๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ์์ ๊ฐ์ด debounce()๋ ์ฌ๋ฌ ๋ฒ ์ด๋ฒคํธ๋ฅผ ํธ์ถํ์ฌ๋ ๊ฐ์ฅ ๋ง์ง๋ง์
ํธ์ถ๋ ์ด๋ฒคํธ๊ฐ ์ ํด์ง ์๊ฐ์ด ์ง๋์ผ ๋ฐฉ์ถ์ด ๋๋ ๊ฒ์ ๋๋ค.
๊ทธ๋ ๊ธฐ์ ํด๋น Operator๋ฅผ ํตํด ์ด์ค ํด๋ฆญ ๋ฐฉ์ง ๊ธฐ๋ฅ์ ๊ตฌํํ ์์๋ ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญ ํ ์ ํด์ง ์๊ฐ์ด
์ง๋์ผ ๋ง ์ด๋ฒคํธ๊ฐ ๋ฒ์ด์ง๋ ์ด์ํ? ์ํฉ์ด ๋ฒ์ด์ง ์ ์์ต๋๋ค. ๐
๊ทธ๋ ๊ธฐ์ ํด๋น Operator๋ EditText๋ฑ๊ณผ ํจ๊ป ์ฌ์ฉํ์ฌ ์ ๋ ฅ๊ฐ์ ๋ํ ์ ํจ์ฑ ์ฒดํฌ๋ฅผ ํ ๋ ์์ฃผ ์ฌ์ฉ๋ฉ๋๋ค. ๐
์์ธ๋ฌ ์ Operator์ ํจ๊ป ์์ฃผ ์ฌ์ฉํ๋ ์์ ฏ์ Operator๋ค์ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
clicks()
View์ ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋ Unit๊ฐ์ฒด๋ฅผ ๋ฐฉ์ถํ๋ Observable์ ์์ฑํฉ๋๋ค.
์ฃผ๋ก Button๊ณผ throttleFirst()์ ํจ๊ป ์ฌ์ฉํฉ๋๋ค.
textChanges()
TextView ์ปดํฌ๋ํธ์ ๋ด๊ธด text๊ฐ ๋ณ๊ฒฝ๋ ๋ ๋ณ๊ฒฝ๋๋ CharSequence๋ฅผ ๋ฐฉ์ถํด์ฃผ๋ operator์ ๋๋ค.
EditText ๋ํ TextView๋ฅผ ์์๋ฐ๊ธฐ ๋๋ฌธ์ ์ฃผ๋ก EditText์ ํจ๊ป ์ฌ์ฉํฉ๋๋ค.
์ด๋ ๊ฒ Operator๋ค์ ๋ํด ์์๋ณด์์ต๋๋ค.
์ด๋ฒ์๋ Rxbinding์ ์ฝ๋์ ์ด๋ป๊ฒ ์ ์ฉ์ํค๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋จผ์ clicks()์ ์ฌ์ฉ๋ฐฉ๋ฒ๊ณผ debounce()์ throttleFirst()๊ฐ ๊ฐ๊ฐ ์ด๋ป๊ฒ ๋ค๋ฅด๊ฒ ๋์ํ๋์ง
์ฝ๋์ ์์์ ํตํด ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค.
์ฐ์ gradle์ dependency๋ฅผ ์ถ๊ฐํด์ค๋๋ค.
dependencies {
...
// RxBinding
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
...
}
<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/debounceClickTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:text="Debounce"
android:textSize="30sp"
android:gravity="center"
/>
<Button
android:id="@+id/debounceButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/debounceClickTextView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="10dp"
android:text="debounce"
/>
<TextView
android:id="@+id/clickTextViewDebounce"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/debounceButton"
android:gravity="center"
android:layout_margin="10dp"
/>
<TextView
android:id="@+id/throttleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/clickTextViewDebounce"
android:gravity="center"
android:text="Throttle"
android:textSize="30sp"
/>
<Button
android:id="@+id/throttleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/throttleTextView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="10dp"
android:text="throttle"
/>
<TextView
android:id="@+id/clickTextViewThrottle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/throttleButton"
android:gravity="center"
android:layout_margin="10dp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private lateinit var mCompositeDisposable: CompositeDisposable
private var mDebounceButtonClicks = 0
private var mThrottleButtonClicks = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
addListeners()
}
override fun onDestroy() {
super.onDestroy()
if(::mCompositeDisposable.isInitialized){
mCompositeDisposable.clear()
}
}
private fun addListeners() {
with(binding){
mCompositeDisposable = CompositeDisposable()
val debounceButtonDisposable = debounceButton.clicks() // Debounce
.debounce(1000 , TimeUnit.MILLISECONDS)
.subscribe({ clickTextViewDebounce.text = (++mDebounceButtonClicks).toString() } , {it.printStackTrace()})
val throttleButtonDisposable = throttleButton.clicks() // throttleFirst
.throttleFirst(1000 , TimeUnit.MILLISECONDS)
.subscribe({ clickTextViewThrottle.text = (++mThrottleButtonClicks).toString() } , {it.printStackTrace()})
mCompositeDisposable.addAll(debounceButtonDisposable , throttleButtonDisposable)
}
}
}
์์ ๊ฐ์ด ์๊ฐ์ ์ค์ ํด์ฃผ๊ณ subscribe()์ ํตํด ๋ฐํ๋ event์์ ํ ๋์์ ์ ์๋ฅผ ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
subscribe์ ๋ค์ด๊ฐ๋ 2๋ฒ์งธ ์ธ์๋ exception handler๋ก
์๋ฌ๊ฐ ๋ฐ์ํ์ ๋์ ๋ํ handler๋ฅผ ์ ์ํด์ฃผ๋ ๋ถ๋ถ์ ๋๋ค.
ํ์๋ exception ๋ฐ์ ์ stackTrace๋ฅผ ์ฐ๋๋ก ํ์์ต๋๋ค.
์์ธ๋ฌ, ์ ๋ณด์ด๋ CompositeDisposable์ ๋ํด์๋ ์๋์์ ํ ๋ฒ์ ์ค๋ช ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
์ ์์์์ ๋ณด์๋ ๊ฒ๊ณผ ๊ฐ์ด
debounce()์ ๊ฒฝ์ฐ์๋ 7๋ฒ์ ๋ฒํผ์ ๋๋ ์์๋ ๋ง์ง๋ง ๋์๋ง ๋ฐํ์ด ๋์ด +1๋ง ํ ๊ฒ์ ๋ณผ ์ ์๊ณ ,
throttleFirst()์ ๊ฒฝ์ฐ 7๋ฒ ํด๋ฆญ ์ ์ฒซ ๋ฒ์งธ ์ด๋ฒคํธ์ ๋ํด์ ๋ฐํ๋๊ณ ๊ทธ ํ 1์ด์ ์๊ฐ์ด ํ๋ฅธ ๋ค์ ๋ค์ด์จ ์ด๋ฒคํธ์ ๋ํด์
ํ๋ฒ ๋ ๋ฐํํ์ฌ +2๊ฐ ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
์ด๋ฒ์๋ textChanges()์ ์ฌ์ฉ๋ฐฉ๋ฒ์ ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
<?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">
<EditText
android:id="@+id/inputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:padding="10dp"
android:layout_margin="10dp"
/>
<TextView
android:id="@+id/verificationTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/inputEditText"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_margin="5dp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private lateinit var mCompositeDisposable: CompositeDisposable
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
addListeners()
}
override fun onDestroy() {
super.onDestroy()
if(::mCompositeDisposable.isInitialized){
mCompositeDisposable.clear()
}
}
private fun addListeners() {
with(binding){
mCompositeDisposable = CompositeDisposable()
val inputEditTextDisposable = inputEditText.textChanges()
.debounce(1000 , TimeUnit.MILLISECONDS , AndroidSchedulers.mainThread())
.subscribe({ verificationTextView.text = String.format("์
๋ ฅ๋ ๊ธ์๋ : %s์
๋๋ค." , it) } , {it.printStackTrace()})
mCompositeDisposable.addAll(inputEditTextDisposable)
}
}
์์์ ์ค๋ช ํ๋ฏ debounce()๋ ๋ง์ง๋ง ์ด๋ฒคํธ๋ง ๋ฐํํ์ฌ ์คํํ๊ธฐ ๋๋ฌธ์ ์์ฒ๋ผ EditText์์ ์ ๋ ฅ์ ๋ง์น๊ณ
1์ด๊ฐ ์ง๋ ๋ค์ TextView์ ๊ฐ์ ๋ณ๊ฒฝ์ํจ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ง์ง๋ง์ผ๋ก Disposable์ ๋ฌด์์ผ๊น์? ๐ค
์ฌ์ ์ ์๋ฏธ๋ก Disposable์ '1ํ์ฑ , ์ผํ์ฉ์'๋ผ๋ ๋ป์ผ๋ก
Observable ๊ฐ์ฒด๋ฅผ ๊ตฌ๋ ํ๊ณ ํด๋น ๊ตฌ๋ ์ด ๋ ์ด์ ํ์ํ์ง ์์ ๋๋ ๊ตฌ๋ ์ ํด์งํด์ผ ํ๋๋ฐ
์ด๊ฒ์ ๋์์ฃผ๋ ๊ฐ์ฒด๊ฐ disposable์ ๋๋ค.
๋ง์ผ, ๊ตฌ๋ ์ ํด์งํ์ง ์๊ณ ๊ณ์ stream์ ๋จ๊ฒจ ๋๊ฒ ๋๋ค๋ฉด
memory leak์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ์์ผ๋ฏ๋ก ๋ ์ด์ ํด๋น ๊ตฌ๋ ์ ๋ํด ํ์ํ์ง ์๋ค๋ฉด
์ ์ ํ๊ฒ ๊ตฌ๋ ์ ํด์งํด์ฃผ์ด์ผ ํฉ๋๋ค.
๊ตฌ๋ ์ ํด์งํ๋ ๋ฐฉ๋ฒ์ผ๋ก๋ 2๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
1. dispose() ํจ์๋ฅผ ํตํด ์ง์ disposable๊ฐ์ฒด๋ฅผ ์ ๊ฑฐํ๋ค.
2. CopositeDisposable๊ฐ์ฒด๋ฅผ ํ์ฉํ์ฌ ํ๋ฒ์ ์ ๊ฑฐํ๋ค.
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
val disposable = binding.button.clicks().subscribe{
// something to do
}
disposable.dispose() // 1๋ฒ ๋ฐฉ๋ฒ
}
}
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private lateinit var mCompositeDisposable: CompositeDisposable
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
val disposable = binding.button.clicks().subscribe{
// something to do
}
mCompositeDisposable = CompositeDisposable(disposable)
}
override fun onDestroy() {
super.onDestroy()
if(::mCompositeDisposable.isInitialized){
mCompositeDisposable.clear() // 2๋ฒ ๋ฐฉ๋ฒ
}
}
๊ฐ๊ฐ์ ์ฌ์ฉ ๋ฐฉ๋ฒ์ ์ ์ฝ๋๋ฅผ ์ฐธ๊ณ ๋ฐ๋๋๋ค.
CompsiteDisposable ๊ฐ์ ๊ฒฝ์ฐ์๋ ๋ชจ๋ Disposable์ ํ ๋ฒ์ ๊ด๋ฆฌ๊ฐ
๊ฐ๋ฅํ๊ธฐ์ ์ฌ๋ฌ ๊ฐ์ง disposable๊ฐ์ฒด๋ฅผ ํด์งํ ๋๋ ํด๋น ๊ฐ์ฒด๋ฅผ ํตํ ๋ฐฉ๋ฒ์ด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค. ๐ค
์ด์์ผ๋ก RxBinding์ ๋ํ ํฌ์คํ ์ ๋ง์น๊ฒ ์ต๋๋ค.
์ค๋๋ ์ฆ์ฝ ํ์ธ์ :)