개발+IT

AAC ViewModel을 사용하는 프로젝트에서 주의할 점

하나에 '문서를' 둘에 '잘읽자'

1. 오류 발생

안드로이드 앱에서 다크 모드를 구현하기 위해 AppCompatDelegatesetDefaultNightMode 함수를 사용할 수 있는데, 이 때 미리 지정해둔 값에 따라 다크 모드와 라이트 모드, 혹은 시스템 설정값을 오가게 된다. 이때 androidx AppCompat 라이브러리 버전 1.1.0-alpha05(릴리즈 노트 확인)부터 Activity recreate 과정을 수행한다. 문제는 이 과정 이후에 Jetpack Navigation을 이용한 Fragment간 화면 이동을 하려는데 오류로 앱이 강제종료가 되는 것이 아닌가. 확인해보니 문제가 된 부분은 Fragment 코드 내의 다음과 같은 부분이었다.

처음에는 Activity가 재생성되면서 기존 Fragment의 생명주기가 꼬인건가? 싶어 FragmentLifecycleCallbacks으로 액티비티 재생성에 따른 변화를 추적해봤는데 별다른 문제를 찾아볼 수가 없었다. 그래서 한참을 삽질하다 보니 Fragment간 화면 이동을 할 때 화면 이동을 지시하는 Fragment와 Activity 재생성 이후에 새롭게 생겨난 Fragment와 서로 다른 인스턴스인 것이 아닌가. 그래서 그 부분을 중심으로 다시 한 번 코드를 꼼꼼하게 보는데...

2. 원인

결론부터 얘기하자면 문제는 ViewModel이 생성될 때 해당 Fragment를 참조하고 있기 때문이었다. 좀 더 정확하게 말하자면 DataBinding XML에서 ViewModel을 참조하고 onClick등의 이벤트를 ViewModel에 선언해둔 함수로 처리하게 하는데, Fragment에서 수행할 수 있는 동작을 위해 ViewModel을 위한 Interface를 만들고 Fragment가 해당 Interface를 구현하도록 한 후 ViewModel에 Fragment를 넘겨주는 것이다. 코드로 보면 다음과 같다

마지막 Fragment 코드에도 써두었지만, navigate 함수에서 NavController를 찾으려는 Fragment가 Activity 재생성 이전의 인스턴스이다. 그렇기 때문에 FragmentManager를 찾을수 없다는 오류가 발생하는 것이었다. 그렇다면 왜 이런 일이 발생하는가? Activity가 재생성되면서 Fragment 역시 재생성되지만, ViewModel은 사라지지않고 유지되기 때문에 처음에 ViewModel이 생성되면서 주입받은 Action을 구현하는 Fragment 인스턴스를 계속해서 들고 있는 것이다.

3. 고민

그렇다면 이런 경우는 어떻게 해결해야 하는 걸까? 

  1. Activity의 재생성에 맞춰서 ViewModel에 들어있는 Action 구현체를 교체해준다.
  2. Activity의 재생성에 맞춰서 ViewModel을 다 날려버리고 새로 생성해준다.

1번과 2번 모두 앱의 작동에는 문제가 없다(아니, 있을수도 있는데 원래 하려던 동작에는 문제가 없다). 하지만 1번 방법의 경우 ViewModel을 사용하는 방법 자체가 너무 번거로워진다. 그렇다면 2번은? 다크 모드 전환 자체가 자주 일어나는 이벤트는 아니겠지만 너무 메모리 낭비인 게 아닐까? 게다가 ViewModel 문서를 보면...

저 빨간 Caution 항목이 보이는가? ViewModel은 view, Lifecycle, 혹은 activity context의 참조를 들고있는 어떤 클래스도 참조하지 않아야 한다고 경고하고 있다. 심지어 ViewModel 문서 극초반에 있는 내용이다. 내가 이걸 제대로 안읽고 만들어서 이 삽질을 한거다. 그래서 이 포스팅의 부제목이

하나에 '문서를' 둘에 '잘읽자'

인 것이다. 여튼 그래서 위의 두 방법은 안되고...어떻게 할까 하다가 든 생각이 바로 다음 장에 있다.

4. 해결

방법은 ViewModel에 LiveData나 RxJava Observable같은 Observe 가능한 객체를 만들고, Fragment에서 이를 구독해서 이벤트를 전달해 필요한 작업을 수행토록 하는 것이다. 이 때 필요한 작업에는 이런저런 인자가 필요할 수 있으니 다음과 같이 구현했다.

일단 여기서는 LiveData로 구현했다. RxJava의 경우에도 비슷하게 만들수 있는데, action을 구독하면서 생기는 Disposable이 Fragment의 생명주기에 따라 적당히 dispose되도록 만들기만 하면 된다. 여튼 여러분은 저처럼 문서를 대충 읽고 삽질하지 않길 바란다.

LiveData의 경우 라이프사이클이 Resume 상태가 되었을 때 값을 한 번 더 읽어오게 되어있으니 의도치 않게 계속해서 같은 액션을 취하게 될 수 있음을 유의하자.

5. 보너스

아래 구매항목에는 별건 아니고 밑에는 RxJava로 구현할 경우에 내가 어떻게 했는지에 대한 코드를 적어뒀다. 여러분의 궁금증이 나에게 커피라도 한 잔 사줄 수 있지 않을까 싶어서 해보았다.

다음 내용이 궁금하세요? 이 포스트를 구매하시면 아래에 이어지는 내용을 감상하실 수 있습니다.

  • 텍스트294
  • 코드35
hell yeah, world
hell yeah, world
구독자 71
멤버십 가입

1개의 댓글

SNS 계정으로 간편하게 로그인하고 댓글을 남겨주세요.
새로운 알림이 없습니다.