Prologue

참고로 해당 글에서 사용되는 코드는 뱅크 샐러드 기술 블로그를 참고하여 작성한 코드입니다.

Android 앱을 사용하다 보면 Back Button 클릭 시, 바로 종료되는 경우도 있고 토스트 메시지를 보여주는 경우도 있다. 사실, 토스트 메시지를 통해 사용자들에게 알려주는 게 더 낫다고 생각하며, 이 기능을 구현하는 방법에 대해 알아보려고 한다.

Subject

이번 포스팅의 큰 주제는 RxJava를 이용하여 Back Button을 처리하는 것이지만, RxJava를 사용하기 전에 우리가 어떻게 처리했는지에 대해 먼저 살펴보자.

Back Button 이벤트 구현

먼저, 물리 버튼인 Back Button의 이벤트를 처리하기 위해 onBackPressed() 함수를 오버라이드 한다. 그리고 기본 로직은 간단하기 때문에 코드와 함께 설명하려고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
private var backPressedTime: Long = 0
val TIME_INTERVAL: Long = 2000

override fun onBackPressed() {
val currentTime = System.currentTimeMillis()
val intervalTime = currentTime - backPressedTime
if (intervalTime in 0..TIME_INTERVAL) finish()
else {
backPressedTime = currentTime
showToast(R.string.desc_exit)
}
}

Back Button을 처음 클릭했을 때, intervalTime을 구하는데 초기 backPressedTime 값은 0이기 때문에 currentTime 그 자체가 된다. intervalTime이 0~2초 사이인 경우, finish()하게 되는데, 현재 시간이 2초보다 클 수 밖에 없다. 따라서 else 문으로 빠지면서 backPressedTime에 currentTime을 저장하고 토스트 메시지를 보여준다.

그리고 연속해서 Back Button을 누르게 되면, currentTime - backPressedTime으로 intervalTime을 구하고 이는 0~2초 사이가 되므로 앱을 종료하게 된다. 코드를 보면 어렵지 않게 이해할 수 있는 내용이다.

Back Button 이벤트 구현 With RxJava

이번에는 똑같은 기능을 RxJava를 이용해서 구현해보려고 한다. 먼저, 해당 글을 읽기 위해서 RxJava의 Subject에 대해서 어느 정도 개념이 잡혀 있어야 한다고 생각한다. 그러므로 이 글을 먼저 읽어본 뒤, 해당 글을 읽는 것을 추천한다.

[1] Subject를 만든다.

  • 뒤로가기 버튼을 클릭했을 때, 이벤트를 emit할 Subject를 생성한다.
  • 참고로 Subject는 Observable이면서 동시에 구독자 역할을 하기 때문에 UI 이벤트 뿐만 아니라 다양한 종류의 item을 발행하고 처리할 때 활용할 수 있다.
  • 뒤로 가기 버튼을 눌렀을 때 발생하는 이벤트의 시간 간격을 판별하는 로직이 필요하고(가장 최근의 이벤트를 emit할 수 있다.) 뒤로가기 버튼을 누를 당시의 기본값이 필요하므로 BehaviorSubject를 생성해준다.
  • Subject에서 데이터를 emit하는 방법은 간단하다. onNext() 함수를 호출하면 된다. 주의할 점은 Subject를 SerializedSubject로 만들어야 한다는 것이다. 이유는 Subject가 Thread Safe하지 않기 때문에 만약, 여러 Thread에서 item을 발행하는 경우 동기화를 보장할 수 없기 때문이다.
  • 여러 Thread에서 접근할 일이 있다면 SerializedSubject로 만들어야 하지만, 여기서 테스트하는 경우에는 그럴 일이 없다. 하지만, 이런 것을 고려해야 한다는 것을 알려주기 위해 toSerialized()를 통해 SerializedSubject로 만들었다.
1
2
3
4
5
6
7
private val backButtonSubject: Subject<Long> by lazy {
BehaviorSubject.createDefault(0L).toSerialized()
}

override fun onBackPressed() {
backButtonSubject.onNext(System.currentTimeMillis())
}

[2] 구독하자.

  • 위에서 뒤로가기 버튼을 클릭했을 때, item을 emit하는 부분을 작업했으니, 구독하는 부분을 구현하면 된다.
  • 아래 코드에서 핵심은 buffer()이다.
  • 참고로 buffer()는 기본적으로 스케줄러 없이 현재 스레드에서 동작한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun initSubject() {
disposable = backButtonSubject
.observeOn(AndroidSchedulers.mainThread())
.buffer(2, 1)
.map { it[1] - it[0] < DELAY }
.subscribe { willFinish ->
if (willFinish) finish()
else Toast.makeText(
this@MainActivity,
R.string.desc_back_button_finish,
Toast.LENGTH_SHORT
).show()
}
}

buffer()는 item는 지정한 갯수만큼 쌓고 있다가, 지정한 갯수가 되면 쌓은 item을 List에 담아서 한번에 emit한다. 이렇게 쌓은 데이터를 통해 it[1] - it[0]을 통해 intervalTime을 구해서 DELAY(2초) 보다 작은지 판단하여 boolean 값으로 변환한다. 결국, 두 이벤트의 시간 차이가 특정 시간인 DELAY보다 짧은 경우 앱을 종료하고, 아닌 경우에는 토스트 메시지를 보여주도록 처리한다.

Back Button을 처리하기 위해 buffer(count, skip) 형태의 함수를 활용했다. buffer(count = 2, skip = 1)은 2개를 쌓으면 item을 emit하고, 기존에 쌓았던 데이터 중 1개를 건너뛰는 걸 의미한다. 따라서 건너뛴 1개는 다음에 item을 쌓을 때 사용된다.

여기서 buffer()에 대해 찾아보면서 count, skip의 갯수에 따라 동작이 다르다는 걸 알게 되었다.
먼저, 위의 경우처럼 buffer(count = 2, skip = 1)의 경우에는 count만큼 쌓으면 item을 emit하고 기존에 쌓았던 데이터 중 1개는 건너뜀으로써 다음에 사용한다.

반면, buffer(count = 2, skip = 3)의 경우에는 count만큼 쌓으면 item을 emit하고 세 번째로 들어오는 item 1개를 skip하여 사용하지 않는다.

필자는 위와 같이 이해를 했습니다. 혹시, 이해를 잘못했을 가능성이 있기 때문에 다른 의견이 있다면 알려주시면 감사를 표하도록 하겠습니다. :-)

요약하면 다음과 같다.

Ref