Post

MVVM과 Android DataBinding

  • MVVM에서의 가장 큰 특징은 옵저빙 이다.

    • 리액티브 프로그래밍에서 말하는 옵저버 패턴과 같은 의미.
  • View가 ViewModel이 가진 데이터를 옵저빙 하고 있다가 ViewModel의 데이터가 변경되면 자동으로 View도 갱신되는 방식
    • 자동으로 View에 notify가 간다고 생각하면 된다.
  • 그래서 ViewModel은 View에 대한 참조를 가지지 않는다. ViewModel입장에서는 내껏만 바꾸면 다른건 신경 안써도 되는거다.
    • (그리고 이 옵저빙 과정에서 발생하는 View->ViewModel의 의존성을 줄이기 위해안드로이드 데이터 바인딩 을 사용한다. 즉, 데이터 바인딩과 MVVM은 별개의 개념이다.)
  • 즉, MVVM으로 컴포넌트를 분리한다는건 기존처럼 Activity(View)에서 ViewModel의 함수를 호출하는 식으로 분리하는 것이 아니다.
    • 물론 이런 식의 호출이 필요한 순간이 있기는 하다만, 이런식의 기존 분리 방식과는 전혀 다르다고 볼 수 있다.
    • 리액티브에서 말하는 옵저버 패턴
databinding
  • 데이터 바인딩은 MVVM과 별개의 개념으로 findViewById 같은 것 없이 바로 xml의 UI 구성요소와 java간의 데이터를 바인딩 할 수 있도록 해준다. 애시당초 그런 기능이다.
    • 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합 할 수 있는 지원 라이브러리입니다.
  • data binding의 장점?
    • 화면 데이터 상태와 세션 데이터 상태가 일치하도록 만들 수 있다.
  • MVVM에서 왜 databinding이 필요한지를 보여주는 예제. 코드가 많이 생략되어 있으니 컨셉만 이해하도록.
    • 단순히 값을 대입하는 작업이 아니라 좀 더 복잡한 작업, 이를테면 어떤 작업의 성공 또는 실패 시 Toast를 띄운다던가 하는 등의 뷰 작업은 Activity에서 .observe() {}를 작성해주어서 데이터 변경 시 트리거 되는 {} 에서 실행하도록 해야 한다.
예제

각 예제가 모두 뷰모델을 어떻게 만들고, 어떤 로직을 액티비티에서 처리하는지 뷰모델에서 처리하는지 뭐 이런 것들이 판이하게 다르다.
서치하다보면 이상한 예제도 너무 많다… 이게 best practice가 아닌 것 같은데?? 싶은 예제들도 있고, 애초에 MVVM을 쓰는데 VM에서 V로의 의존성이 강하게 존재하는 코드들도 있고… 이러면 MVVM을 쓰는 의미가 없지 않나 싶은 예제들이…
아무튼,
제일 확실한건 안드로이드의 공식 예제임. 그걸 따라가도록 한다.

데이터 바인딩 (databinding) 사용법

databinding 의존성 추가하기 구글 docs 아래 설정이 끝이다.

1
2
3
4
5
6
7
8
apply plugin: 'kotlin-kapt'
 
android {
    ...
    dataBinding {
        enabled = true
    }
}

kapt는 annotationProcessing을 대체하는 코틀린 애너테이션 Processor

[TED]데이터 바인딩의 기초부터 예제까지. @BindingAdapter와 @BindingConversion. MVVM과는 전혀 관련이 없으니 주의.

데이터 바인딩의 동작 방식 조금 더 상세

1
2
3
4
5
6
7
<data>
    <variable
        name="vm"
        type="com.tistory.umbum.github_issue_widget_app.viewmodel.RepoSelectViewModel" />
</data>
<android.support.v7.widget.RecyclerView
            android:id="@+id/repo_list_view" />
1
2
3
다음과 같이 접근 가능하다
binding.vm              // type은 <RepoSelectViewModel>
binding.repoListView    // type은 <RecyclerView>

Model -> ViewModel -> View로의 데이터 파급

1
2
3
4
5
6
class UserProfileViewModel @Inject constructor(
       savedStateHandle: SavedStateHandle,
       userRepository: UserRepository) : ViewModel() {
       val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id")
       val user : LiveData<User> = userRepository.getUser(userId)
}

여기서 userRepository.getUser(userId)가 LiveData를 리턴하면 곧바로 user에 LiveData 객체가 할당되고, 이후 Repository의 callback이 완료되는 순간 Repository에서 LiveData 변수에 값을 할당하면 이 변경 사항이 ViewModel의 LiveData 변수에 파급되고, 뒤이어 View에도 파급된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserRepository {
       private val webservice: Webservice = TODO()
       // ...
       fun getUser(userId: String): LiveData<User> {
           // This isn't an optimal implementation. We'll fix it later.
           val data = MutableLiveData<User>()
           webservice.getUser(userId).enqueue(object : Callback<User> {
               override fun onResponse(call: Call<User>, response: Response<User>) {
                   data.value = response.body()
               }
               // Error case is left out for brevity.
               override fun onFailure(call: Call<User>, t: Throwable) {
                   TODO()
               }
           })
           return data
       }
}
Model은 자기 자신에 대한 데이터를 유지하고 있는게 맞나?
1
2
3
4
5
6
7
View  ------------->  ViewModel  <------------  Repository
     (field를 observe)          (Model을 return
                                  field에 할당)
                                1. LiveData를 리턴?
                                == 액티비티가 부셔지면 같이 없어져야 하는 경우.
                                2. Observable을 리턴?
                                == Rx를 사용할 때.

또는 Repository 없이 바로 ViewModel에서 Model을 생성해도 무관하다.

Repository의 역할은 여러 데이터 소스로부터 Model을 가져오는 것을 추상화하는 역할 이므로, 단일 데이터 소스로부터 가져와서 별다른 추상화가 필요하지 않다면 ViewModel에서 바로 retrofit같은걸 써서 가져와도 괜찮음.

This post is licensed under CC BY 4.0 by the author.