Naver Tech Concert Day-1

03. MVVM with Grab Architecture

  • 발표자 : 정승욱 (Grab / 안드로이드 개발)
  • 동영상 : https://tv.naver.com/v/4637223
  • 슬라이드 : https://www.slideshare.net/NaverEngineering/12mvvm-grab-architecture-mvvm
  • 세션설명 : 구글이 Android Architecture Component 의 ViewModel 을 발표하면서 다양한 시각의 MVVM 구현이 제시되고 있습니다. 여전히 많은 사람들이 혼동하는 MVVM 구현에 대해 올바른 안드로이드 MVVM 구현을 공유하고자 합니다. 이를 위해 안드로이드에서 어떠한 기본 작업이 선행되어야 하는지, 다양한 문제 상황에 대한 해결책에 대해 알아보고 MVVM 이 Grab 에서 어떻게 활용되고 있는지 알아보겠습니다.

국내 MVVM 이해도 오류가 많음. (국내 포스팅 등...)

예로 아래의 경우 뷰에서 옵저빙을 하는 부분이 문제임.

class MainViewModel {
    val title LiveData<String>
}
...
class MainActivity {
    val viewModel: ViewModel
    val textView: TextView
    fun onCreated() {
        viewModel.title.observe(this) {
            textView.text =it
        }
    }
}

Android Architecture Component의 ViewModel

AAC의 ViewModel은 MVVM의 ViewModel과는 무관함.

AAC의 ViewModel은 Activity와 Fragment의 Life Cycle 의존성을 낮추는 것.

LiveData는 Repository로부터 데이터 변화 반응/적용이 목적이며 ViewModel은 LiveData로부터 View에 필요한 데이터를 관리함.

MVVM의 ViewModel

View와 ViewModel 연결 최소화

ViewModel은 데이터의 변화를 View에 전달

View는 화면 정보의 변화를 ViewModel에 전달

위에서 잘못된 코드는 아래와 같이 변경되어야 함.

class MainActivity {
    val viewModel: ViewModel    
    fun onCreated() {
        dataBinding.setVariable(BR.vm, viewModel)
    }
}

안드로이드에서 DataBinding 사용에 문제 사항들

  1. LifeCycle
  2. Databinding으로 다 표현하기 힘든 View 이벤트
  3. Resource 등 Context를 접근해야 하는 경우

LifeCycle

그랩에서는 LifeCycle 문제를 위해 RxBinder를 만들어 사용함. (Trello의 RxBinder 참조)

class ViewModel(val rxbinder: RxBinder) {
    init {
        rxbinder.bind(ON_DESTROYED) {
            Observable....
        }
    }
}
...
class RxBinder {
    val map: Map<Event, CompositDiposable>
    fun apply(lifeEvent: Event) {
        map[lifeEvent]?.clear()
    }
    fun bind(lifeEvent: Event, body:()->Disposable) {
        map[lifeEvent].add(body())
    }
}
...
open class RxActivity{
    val rxbinder: RxBinder
    fun onCreated() {
        rxbinder.apply(ON_CREATED)
    }
}

View 변화 감지

GlobalLayoutChangeListener 등은 데이터 바인딩으로 구현이 어려움.

위의 경우 2-way 바인딩 구현해야 하며 3개의 function을 구현해야 하며 이해가 어려움.

이를 위해 2-way 바인딩 구현보다는 usecase를 만들어 회피 사용함.

class GetVisibleAreaUsecase(view1) {
    fun observe(): Observable<Rect> {
        return Observable.create {e->
                                  e.onNext(view1.height)
                                  view1.addGlobalLayoutChange{
                                      e.onNext(view1.height)
                                  }
		}.distictUntilChanged()
    }
}
...
class ViewModel(usecase:GetVisibleAreaUsecase) {
    init{
        usecase.observe().subscribe{
            /* do something */
        }.bindUntil(rxbinder)
    }
}

Resource 접근

ResouceProvider 라는 랩핑을 만들어 context를 전달해 사용

class ResourceProvider(context: Context) {
    fun string(resId: String) = context.resource.getSting(resId)
}

ActivityResult는 매니저를 만들어 Activity에서 메소드 실행시 ViewModel에서 구독됨.

class Activity {
    val resultManager
    fun onActivityResult() {
        resultManager.onResult()
    }
}
...
class ResultManager{
    fun listen(observer)
    fun onResult()
}
...
class ViewModel(resultManager) {
    init{
        resultManager.listen {
            // do something
        }
    }
}

위의 모든 것이 구현된다면 대략...

class ViewModel(rxbinder, usercase, resultManager) {
    init{
        // many things are here
    }
}
...
class Activity: ResultableRxActivity {
    fun onCreated() {
        dataBinding.setVariable(BR.vm, viewModel)
    }
}

실제 액티비티의 역할은 아래 정도?

액티비티에서 DataBinding과 DI만?

class MainActivity: RxResultableActivity{
    @Inject lateinit var vm: MainViewModel
    fun onCreated() {
        dataBinding.setVariable(BR.vm, vm)
    }
}

그랩에서는 단일 Layout을 조각내기로 함.

한 레이아웃을 뷰를 나눠 각각을 노드화하여 처리.

각 뷰 별로 바인딩 노드가 되고, 각 노드에 ViewModel이 인젝션됨.

바인딩 노드는 부모뷰와 ViewModel을 받음.

1. Activity

class ConfirmActivity{
    fun onCreated() {
        BackButtonNode({parentView}).dependency(root).build()
        TaxiTypeNode({parentView}).dependency(root).build()
        ExtraInfoNode({parentView}).dependency(root).build()
        PayConfirmNode({parentView}).dependency(root).build()
        //...etc...
    }
}

2. 5번 TaxiTypeNode

class TaxiTypeNode(parentView:()->ViewGroup): BindingNode{
    @Inject lateinit var vm: ViewModel
    fun build() {
        depdency.inject(this)
        binding(parenteView(), vm)
    }
}

3. BindingNode

open class BindingNode {
    fun binding(parentView: ViewGroup, vm: ViewModel) {
        val binding = ViewBinding.inflate(vm.layoutId, parentView)
        binding.setVariable(BR.vm, vm)
    }
}

4. Root Parent XML

<FrameLayout>
    <FrameLayout android:id="@+id/parent1" />
	<FrameLayout android:id="@+id/parent2" />
</FrameLayout>

노드화의 장단점

  • 장점 : Node 제어로 View 플러그인화 가능
  • 단점 : BackKey, Save/Restore Instance 등 다양한 처리 구현

위 구조는 작은 사이즈에서 중간 규모의 앱에서는 과도하며, 인터랙션이 많거나 View 자유도, 재활용이 높은 앱에 권장함.

정리

View는 XML

DataBinding 필수요소

감지하기 어려운 뷰 변화는 ViewUsecase

LifeCycle, Result 위한 처리 필요

Resource 접근은 Wrapper 처리

Node 예제는 Optional

Q&A

Q : 그랩에서는 AAC 사용 안하는지?

데이터 용도로 맞지 않고 라이프사이클 처리도 직접 함으로 Room, LiveData, AAC의 ViewModel 등은 사용 안함.

Q : 노드 사용 사례는?

루트 노드에서 라우터가 하위 노드를 컨트롤하며, 하위 노드를 구성하도록 지시함.

Q : 이재원님(MS Expert)의 안드로이드의 MVVM 오류에 대한 내용이 반영되었는지?

반영된 내용임.

Q : RxBinding이 그랩에서 자체적으로 만든건지?

외부(Trello)에서 아이디어만 가져오고 자체 구현함.

Q : Activity에서 ViewModel을 직접 참조할 일이 없다는데? Activity에서 반응에 대한 부분을 ViewModel에 전할때는?

데이터 바인딩으로 처리됨.

Q : 노드의 상대적인 위치는 어떻게 지정?

루트노드에서 Parent 대비 상대적인 위치가 지정되어 있음.

Q : 전통적인 Activity와의 차이는?

레이아웃 부분이 static으로 지정되어 있지만, 노드에서는 노드 단위로 다른 화면에서도 재활용이 가능함.

Q : 빈 레이아웃에 attach/dettach 하는 방식이 Fragment와 비슷한데?

그랩에서는 Fragment를 사용안함. onRestore 시 등 재생성 충돌등의 문제가 있어 별도로 노드 처리함.

Q : 기본 선수 지식은?

DataBinding(2-way까지), MVP에 대한 깊은 경험, MVP에서 MVVM으로 변화에 대한 이해 필요.

Q : ViweModel에서 Resource ID를 가지는 이유는?

ViewModel에서 어떤 리소스와 매칭되는지 판단을 했지만, 편의상 Node보다 ViewModel에서 가지고 있음.

Q : 2-way 바인딩 사용 사례는?

2-way 바인딩이 한번 이상 재사용시에는 2-way 바인딩을 빼서 사용을 권고함.

Q : DataBinding이 MVVM에 필수인데 Anko의 경우는?

Anko같은 경우는 네이티브 코드이고 Anko로 작성한다 해도 중간에 추상화 코드가 나오는 게 아니라 현재 구현된 상태가 최선인 듯. 현재로썬 Anko와는 궁합이 좋지 않음.

Q : AlertDialog는 어떻게 처리?

AlertDialog도 노드로 처리

Q : 싱글 액티비티에서 히스토리 관리는?

상위 노드에서 관리하며, 히스토리 관리하는 모듈이 있음.

Q : 그랩은 모두 싱글액티비티임?

메인 플로우들은 싱글 액티비티이나 페이먼트, 리워드 등은 아님. 그래서 onResult 처리가 필요했음.

Q : Trello RxLifeCycle에서 추가된 기능은?

자체적으로 필요한 부분만 추가되고 Trello에서는 필요한 것만 빼서 씀

Q : MS 제안한 MVVM 의도대로 분업이 되었는지? (XML은 디자이너, ViewModel은 엔지니어)

그랩에선 디자이너는 UX/비주얼(7:3정도) 디자이너로 나뉘며, xml이나 ViewModel 모두 엔지니어가 작업

Q : Save Instance 상태는 어떻게 관리하는지?

노드별로 저장해서 상위 노드에 전달하고 복원 시작시 하위 노드에 다시 전달함. 복잡한 부분이라 라이브러리화해서 사용 중.


+ Recent posts