꼭 이번주에 있었던 문제는 아니지만, 최근 개발하다가 삽질한 이력을 남겨본다.
View들의 Visibility를 데이터 바인딩을 이용해 변경할 수 있다. 아래 코드를 보자.
android:visibility="@{uiVisible ? View.VISIBLE : View.GONE}"
위처럼 uiVisible
를 할당하면 Activity나 Fragment에서 uiVisible
을 true / false로 바꿔주기만 해도 visibility가 업데이트 된다.
특정 버튼(btn_open)을 누르면 EditText와 soft keyboard가 뜨고 EditText에 Focus를 줘야했다. EditText역시 위처럼 데이터 바인딩을 사용하고 있었고 아래와 같이 작성했다.
btnOpen.setOnClickListener {
editTextVisible = true
showKeyboard()
editText.requestFocus()
}
그냥 보면 EditText가 보여지고, soft keyboard가 올라오고, EditText에 포커스가 생길 것으로 보인다.
하지만, 실제로는 EditText에 포커스가 생기는 일은 발생하지 않았다.
원인은 editText.requestFocus()
가 불린 순간 EditText는 아직 VISIBLE이 아니었던 것이었다. editText.requestFocus()
는 GONE인 EditText에 Focus를 줄 수 없었던 것이다.
사실 데이터 바인딩은 변경 사항을 스케쥴링하여 다음 프레임에 반영되도록 하는 방식으로 되어있다.
즉, 데이터(editTextVisible)을 변경했다고 UI도 곧바로 적용되는 것은 아니다. 결국, 위의 코드대로면 다음 프레임에 EditText가 VISIBLE이 될 것이라고 예정될 뿐이지, VISIBLE이 되지는 않는다.
작동원리를 몰랐던 것은 아니지만, 코딩할 때는 editTextVisible = true
을 editText.visibility = View.VISIBLE
와 같은 것으로 생각하는 실수를 했던 것이다.
첫번째로는 executePendingBindings()
를 호출해 즉각적으로 변경 사항을 적용시키는 것이다. 다음 프레임에 적용시킬 것을 강제로 즉시 반영시키면, EditText가 즉시 VISIBLE이 되고, requestFocus()
는 성공한다.
btnOpen.setOnClickListener {
editTextVisible = true
executePendingBindings()
showKeyboard()
editText.requestFocus()
}
둘째로는 아예 EditText를 커스텀 뷰로 만들어 setVisibility()
를 오버라이드 하는 것이다. 이경우 visibility가 VISIBLE로 바뀔 때는 항상 focus가 필요했다. 실제로 다른 이유로 EditText는 커스텀 뷰로 만들어 쓰고 있었기에 setVisibility()
를 아래처럼 오버라이드 했다.
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
if (visibility == View.VISIBLE)
requestFocus()
}
RxJava는 subscribeOn()
과 observeOn()
함수에 Scheduler를 인자로 줘서 원하는 스레드 풀의 스레드로 실행 흐름을 정할 수 있다. subscribeOn()
로는 최초 스레드를 지정할 수 있으며, 체이닝 거는 위치는 상관없다. 반면 observeOn()
로는 체이닝을 한 이후의 스레드를 지정할 수 있고, 체이닝을 거는 위치가 중요하다.
주로 사용하는 스케쥴러로는 AndroidSchedulers.mainThread()
, Schedulers.computation()
, Schedulers.io()
가 있다. 많은 계산이 동반되는 경우 computation을 사용하고, 네트워크 사용이나 내부 DB에 접근할 때 io를 사용하고, Ui에 적용할 때 mainThread로 돌아온다고 생각하면 될 것이다.
firebase서버에 접근(compF)한 뒤 retrofit을 사용해서 요청(compR)하는 경우가 있었고, Completable을 andThen으로 이어 붙이는 방식으로 Completable을 생성했다. io관련 작업이므로 subscribeOn
은 io로, observeOn
은 mainThread로 설정했다. 아래와 같은 꼴로 작성했다.
compF
.andThen(compR)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...)
firebase 관련 로직은 요청을 CompleteListener를 등록하고, 받은 결과를 create -> emitter로 처리했다. 아래와 비슷한 꼴을 생각하면 된다.
Completable.create { emitter ->
firestore
.(somePath then request)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
emitter.onComplete()
} else {
emitter.onError()
}
}
}
retrofit은 결과를 Completable로 받는 식이었다.
fun request(...): Completable
그런데 막상 실행한 결과는 mainThread에서 네트워크를 돌렸다는 Exception의 발생이었다. 분명 io를 지정했는데 실제로는 io에서 동작하지 않은것이다.
우선 Firebase의 addOnCompleteListener()
로 등록된 람다함수가 어떻게 동작하는지를 간단하게나마 이해할 필요가 있다.
사실 Firebase는 위에서 설정한 .subscribeOn(Schedulers.io())
가 무색하게 서버와 통신은 Firebase가 별도 관리하는 스레드에서 진행한다. 그리고 Listener에 등록했던 람다함수, 즉 emitter.onComplete()
는 mainThread에서 실행한다.
사실 .subscribeOn(Schedulers.io())
가 의미 없고, mainThread에서 emitter.onComplete()
가 호출되는 것이 무슨 상관이냐고 할 수 있다.
하지만 이후의 흐름은 이전에 설정된 Scheduler와 무관하게, 실제 배출(emit)이 일어난 스레드를 따른다. 다시 말하면, 배출(emit)된 아이템은 이후 실제 배출(emit)이 일어난 스레드에서 실행된다.
정리하면, firebase 요청의 결과로 emitter.onComplete()
는 mainThread에서 호출되었다. 그리고 andThen으로 되어있던 retrofit 연산은 실제 배출(emit)이 일어난 mainThread에서 실행된 것이다. (우리가 지정했던 Schedulers.io()
는 무시한채로)
먼저 가장 간단한 방법은 Retrofit 객체를 만들 때, Retrofit 관련 작업 스레드를 지정해주는 것이다. Retrofit를 빌더 패턴으로 만들 때 아래와 같이 넣어주면 된다.
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
이렇게 지정해두면 firebase 때문에 mainThread에서 emitter.onComplete()
가 불려도 retrofit 작업을 할 때는 io로 넘어가게 되고, Exception은 발생하지 않는다.
둘째는 observeOn()
을 retrofit 작업 전에 걸어 다시 Scheduler를 설정하는 것이다.
compF
.observeOn(Schedulers.io())
.andThen(compR)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...)
이러면 compF에서 mainThread에서 emitter.onComplete()
가 불려 mainThread로 넘어가도, retrofit 관련 작업은 io로 넘어가게 되어 Exception이 발생하지 않는다.
그외에도 선후관계가 반드시 필요한게 아니면 두 Completable을 merge로 묶어 처리할 수 있다.