Hun's Blog

개인 프로젝트(Bookmark-kotlin)를 통해 알아보는 ViewModel의 생성 본문

Android

개인 프로젝트(Bookmark-kotlin)를 통해 알아보는 ViewModel의 생성

jhk-im 2020. 12. 4. 11:12
 

ViewModel 개요  |  Android 개발자  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com

 

ViewModel

본래  MVVM 디자인 패턴의 ViewModel 에서 파생되었으며 Android에서는 Jetpack에 포함된 ViewModel기능을 제공한다. 

 

안드로이드의 개발 환경을 살펴보면 XML을 사용하여 레이아웃과 같은 View를 표시하게 되는데 Button의 on/off와 같은 사용자에 의한 간단한 UI 상호작용을 담당하며 이를 UI Controller(Activity, Fragment..)내부에서 View로부터 발생한 상호작용과 Model에서 가지고있는 데이터 사이에서 복합적인 상호작용을 구현하여 다양한 화면을 구성하게 된다. 여기서 시간이 지날수록 UI Controller에 방대한 코드가 자리잡게 된다.

 

MVVM은 View와 ViewModel을 분리하는데 이는 Activity, Fragment와 같은 UI Controller에 쌓이게 되는 코드를 분담하여 유지보수 및 재사용성, 테스트를 용이하게 해준다. UI Controller를 View의 일부분으로 묶기 때문에 UI Controller 내부에 뷰를 표시하는 것 이외에 모델과의 상호작용하는 로직을 ViewModel에서 구현하는 것이 원칙이다. 

 

다시한번 처음으로 돌아가서 Android 에서는 Jetpack에 ViewModel이 포함되어있다. 이는 Android의 개발환경에 알맞게 설계되어 제공된다는 의미일 것이다. 어떠한 기능이 있는지 공식문서의 내용을 통해 정리해보도록 하겠다. 

* Android Architecture ViewModel 이라해서 ACC ViewModel이라 부른다. 

 

ACC ViewModel

ACC ViewModel 클래스는 안드로이드 생명주기를 의식하여 UI와 관련된 데이터를 저장하고 관리하도록 설계되었다. 안드로이드 프레임워크는 Activity, Fragment와 같은 UI Controller의 생명주기를 관리하는데 이는 다음과 같은 문제가 있다. 

 

1. UI Controller를 파괴하거나 다시 생성하면 저장되었던 모든 UI관련 데이터가 손실된다.

2. UI Controller가 비동기로 구현되어야 할때 다소 시간이 걸릴 수 있다. 

3. UI Controller가 데이터베이스, 네트워크 로딩을 담당하는 경우 클래스가 거대해진다. 

4. 과도한 책임을 부여받은 클래스는 테스트를 어렵게 만든다. 

 

그렇기 때문에 View 데이터관련 로직을 UI Controller에서 분리하는 것이 효율적이다. 

 

다음은 ACC ViewModel에 대한 설명이다.

1. LifecycleOwner 혹은 특정 View 인스턴스보다 오래 지속되도록 설계되었다. 

2. Lifecycle혹은 View 객체를 모르더라도 테스트코드를 쉽게 적용할 수 있다. 

3. LiveData 객체와 같은 LifecycleObserver를 포함할 수 있다.

4. 생명주기를 인식하는 Observable의 변경사항을 관찰하면 안된다. 

 

ACC ViewModel 생명주기 

ViewModel 객체의 범위는 ViewModelProvider에 전달되는 Lifecycle로 지정된다. ViewModel은 범위가 지정된 Lifecycle이 끝날 때까지 메모리에 남아있으며 Activity가 종료되거나 Fragment의 경우 분리될 때 까지 메모리에 남아있다.

 

다음의 이미지는 Activity의 Lifecycle이 지정된 ViewModel의 주기를 나타낸다.  Fragment에도 동일하게 적용된다.

https://developer.android.com/topic/libraries/architecture/viewmodel

일반적으로 Activity의 onCreate() 메서드가 호출될 때 ViewModel을 요청한다. ViewModel이 처음 요청되고나서 Activity가 onDestroy() 될 때까지 존재한다. 

 

Fragment 사이에 데이터 공유 

Activity에 속한 두개 이상의 프래그먼트간의 데이터를 공유할 때 각각 인터페이스 설명을 정의해야 하며, Owner Activity에서 두가지를 함께 묶어야 하므로 간단히 처리할 수 있는 작업이 아니다. 또한 다른 프래그먼트가 아직 생성되지 않았거나 표시되지 않은 시나리오도 처리해야 한다. 이러한 고충을 ViewModel 객체를 사용하여 해결할 수 있다. 

 

두 프래그먼트 모두 자신이 포함된 Activity를 검색하고 각 프래그먼트에서 ViewModelProvider를 가져올 때 Activity로 범위가 지정된 동일한 ViewModel 인스턴스를 받는다. 

 

이러한 접근 방법은 다음과 같은 이점이 있다. 

  • Activity는 아무것도 하지 않아도 되거나 데이터 공유에 관해 어떤 것도 알 필요가 없다. 
  • 각 프래그먼트는 ViewModel외에 서로에 관해 알 필요가 없다. 프래그먼트가 사라져도 다른 프래그먼트는 평소대로 동작한다. 
  • 각 프래그먼트에 자체 생명주기가 있으며, 다른 프래그먼트의 생명주기에 영향을 받지 않는다.  

 

여기 까지는 안드로이드 공식 문서에 나오는 ViewModel에 관한 설명이었다. 이제 프로젝트에서 ViewModel이 어떻게 활용되는지 살펴보고 위에서 설명하는 내용이나 이점들이 잘 녹아들었는지 살펴보도록 하겠다. 

 

 

BookmarkActivity

class BookmarkActivity : AppCompatActivity(), BookmarkNavigator, BookmarkItemNavigator {

  private lateinit var viewModel: BookmarkViewModel

  @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    overridePendingTransition(R.anim.fadein, R.anim.fadeout)
    setContentView(R.layout.bookmark_act)
	
    ...

    viewModel = obtainViewModel().apply {
		...
    }
  }
  
  fun obtainViewModel(): BookmarkViewModel =
    obtainViewModel(BookmarkViewModel::class.java, this)
   
   ...
  }

 우선 액티비티가 생성될 때 obtainViewModel() 메서드를 활용하여 ViewModel을 생성한다. 

obtainViewModel()메서드는 동일한 이름의 메서드를 통해 ViewModel을 생성하고 반환한다. 메서드에는 생성될 ViewModel 클래스와 현재 액티비티를 입력한다. 

해당 메서드는 다음과 같은 경로에 선언되어 있다.

// util/AppcompatActivityExt.kt

...
fun <T : ViewModel> AppCompatActivity.obtainViewModel(
  viewModelClass: Class<T>,
  owner: ViewModelStoreOwner
) =
  ViewModelProvider(owner, ViewModelFactory.getInstance(application)).get(viewModelClass)

위에서 보았던 공식문서와 같이 ViewModelProvider를 통해 ViewModel을 생성한다.

중간에 있는 ViewModelFactory를 통해  Factory를 생성하고 입력하게 된다. 이부분을 조금 더 자세히 살펴보도록 하자. 

 

ViewModelFactory

일반적으로 ViewModel을 생성하기위한 코드는 다음과 같다.

viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
		.get(MainViewModel::class.java)

 

ViewModelFactory는 위와 동일하게 ViewModelProvider.NewInstanceFactory()를 통해 생성되는 Factory를 반환한다. 다만 

Factory가 생성될 때 사용되는 create 함수를 오버라이드하고 재정의하여 사용한다. 해당 부분을 살펴보도록 하겠다. 

class ViewModelFactory private constructor(
  private val itemsRepository: ItemsRepository
) : ViewModelProvider.NewInstanceFactory() {

  override fun <T : ViewModel?> create(modelClass: Class<T>) =
    with(modelClass) {
      when {
        isAssignableFrom(BookmarkViewModel::class.java) ->
          BookmarkViewModel(itemsRepository)
        isAssignableFrom(AddEditBookmarkViewModel::class.java) ->
          AddEditBookmarkViewModel(itemsRepository)
        isAssignableFrom(BookmarkDetailViewModel::class.java) ->
          BookmarkDetailViewModel(itemsRepository)
        isAssignableFrom(DeleteBookmarkViewModel::class.java) ->
          DeleteBookmarkViewModel(itemsRepository)
        else ->
          throw IllegalArgumentException("Unknown ViewModel Class: ${modelClass.name}")
      }
    } as T

  companion object {

    @SuppressLint("StaticFieldLeak")
    @Volatile
    private var INSTANCE: ViewModelFactory? = null

    fun getInstance(application: Application) =
      INSTANCE ?: synchronized(ViewModelFactory::class.java) {
        INSTANCE ?: ViewModelFactory(
          Injection.provideBookmarkRepository(application.applicationContext)
        ).also { INSTANCE = it }
      }

    @VisibleForTesting
    fun destroyInstance() {
      INSTANCE = null
    }
  }
}
  • 싱글턴으로 되어있는 ViewModelFactory는 인스턴스가 생성될 때 Injection클래스를 통해 ItemRepository를 주입한다. 해당 부부은 나중에 자세히 다뤄 보도록하겠다. 
  • ViewModelFactory가 생성될 때 오버라이드 된 create 메서드를 통해 ViewModel 클래스를 구분하고 현재 생성해야 할 ViewModel을 선택하여 생성한다. 
  • ViewModel 클래스의 구분은 ViewModelProvider에서 .get() 메서드로 입력되는 ViewModel 클래스에 의해 선택된다. 

결과적으로 위와같은 흐름을 활용하면 각각 다르게 구현된 프래그먼트와 ViewModel을 불필요한 코드의 반복없이 obtainViewModel()에 입력하는 것 만으로 ViewModel을 생성할수 있다.  

 

이렇게 생성된 ViewModel은 프래그먼트의 onCreateView() 에서 다음과 같이 연결된다.

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    viewDataBinding = BookmarkFragBinding.inflate(inflater, container, false).apply {
      viewModel = (activity as BookmarkActivity).obtainViewModel()
    }
    setHasOptionsMenu(true)
    return viewDataBinding.root
  }

databinding의 viewModel에 액티비티의 obtainViewModel()을 통해 얻은 ViewModel의 인스턴스를 입력하여 연결한다. 

 

 

https://github.com/jhk-im/bookmark-kotlin-mvvm

 

GitHub - jhk-im/bookmark-kotlin-mvvm: bookmark se refactoring with kotlin

bookmark se refactoring with kotlin. Contribute to jhk-im/bookmark-kotlin-mvvm development by creating an account on GitHub.

github.com