Hun's Blog

Android Dagger-Hilt 적용기 (4) - MVVM 본문

Android

Android Dagger-Hilt 적용기 (4) - MVVM

jhk-im 2020. 11. 11. 00:41

아래의 오픈소스를 통해서 분석하고 학습하여 개인프로젝트에 Dagger-Hilt적용

github.com/android/architecture-samples/tree/dev-hilt

 

android/architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - android/architecture-samples

github.com

github.com/skydoves/Pokedex

 

skydoves/Pokedex

🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture. - skydoves/Pokedex

github.com

MVVM에서 Model은 이전에 만들어 두었으니 View와 ViewModel을 만들어보자. 

View 

@AndroidEntryPoint
class MainActivity : DataBindingActivity() {

  @VisibleForTesting val viewModel: MainViewModel by viewModels()
  private val binding: ActivityMainBinding by binding(R.layout.activity_main)

  override fun onCreate(savedInstanceState: Bundle?) {
    onTransformationStartContainer()
    super.onCreate(savedInstanceState)
    binding.apply {
      lifecycleOwner = this@MainActivity
      adapter = PokemonAdapter()
      vm = viewModel
    }
  }
}

@AndroidEntryPoint

dagget-hilt의 annotation

다음의 클래스를 먼저 보도록하겠다. 

@HiltAndroidApp
class PokedexApp : Application()

공식 문서 : `Application에 @HiltApplication 으로 hilt를 설정하면 @AndroidEntryPoint 주석이 있는 다른 클래스에 종속항목을 제공할수 있다`  

 

View를 먼저 파악하기위해 viewModel은 나중으로 미루고 ActivityMainBinding에 대해 정리해보자면 

MainActivity와 연결되어있는 main_activity.xml를 다음과 같이 설정하면 DataBinding을 사용할 수 있다. 

<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools">

  <data>

    <variable
      name="vm"
      type="com.skydoves.pokedex.ui.main.MainViewModel" />

    <variable
      name="adapter"
      type="com.skydoves.pokedex.ui.adapter.PokemonAdapter" />
  </data>
  ...
  </layout>

 

액티비티에서 DataBinding을 연결하는 일반적인 코드는 다음과 같다. 

private lateinit var binding: MainActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.main_activity)
    }

다시 오픈소스 액티비티를 살펴보면 일반적으로 AppCompatActivity() 가 아닌 DataBindingActivity()를 상속받고 있다. 

private val binding: ActivityMainBinding by binding(R.layout.activity_main) 

위 코드가 액티비티와 xml을 연결해주고 있다. 

binding() 메서드를 따라가보면 다음과 같은 로직을 만날 수 있다. 

abstract class DataBindingActivity : AppCompatActivity() {

  protected inline fun <reified T : ViewDataBinding> binding(
    @LayoutRes resId: Int
  ): Lazy<T> = lazy { DataBindingUtil.setContentView<T>(this, resId) }
}

해당 클래스는 기본으로 액티비티가 상속받아 사용하는 AppCompatActivity()를 상속받고있다. 

 

protected -> private + subclass 에서 사용할 수 있음

 

inline fun -> 함수를 인자로 전달하는 lamda expression 을 정의할 때 해당 로직이 자바로 변환되고나서 람다로 선언된 메서드를 사용하기 위해 객체를 생성해야하고 이는 성능을 떨어뜨릴 수 있다. 이때 inline fun 키워드를 사용하면 객체를 생성하지 않고 호출하는 위치에 복사한다. 바이트 코드의 양은 많아지지만 추가적으로 객체생성을 하지 않기에 성능저하를 막을 수 있다. 

 

reified -> Generic에서 <T>는 런타임에서 접근할 수 없다. 접근하고 싶은 경우 메서드를 만들고 파라미터로 타입을 전달해주어야 한다. inline fun 메서드에서 refied type 을 사용할 경우 추가 메서드 없이 런타임에서 <T>에 접근할 수 있다. reified는 inline fun 조합에서 사용할 수 있다. 

 

layz -> lateinit 과 마찬가지로 초기화를 지연시킬 때 사용한다. 두가지는 다음과같은 차이점이있다. 

  • lateinit은 var 타입만 가능하고 lazy는 val 타입만 가능
  • lateinit은 primitive type은 불가능하나 lazy는 가능
  • lateinit은 Non-null 타입만 가능하나 lazy는 둘 다 가능
  • lateinit은 로컬 변수에서는 불가능 하나 lazy는 가능

lateinit 으로 선언한 코드에서 초기화 코드를 빼먹으면 런타임 에러가 발생한다. 이를 방지하기 위해 layz를 사용하는 것이 적절하다. 오픈소스의 lazy { DataBindingUtil.setContentView<T>(this, resId) } 와 같이 lazy는 블록안에 초기화코드가 함께 있다. 

 

이제 액티비티로 돌아와 databinding이 연결되는 코드를 다시한번 살펴보자.

private val binding: ActivityMainBinding by binding(R.layout.activity_main)

private val binding: ActivityMainBinding by binding(R.layout.activity_main)

by ->   객체지향에서 상위 클래스 내용이 변경되는 경우 하위 클래스가 상위 클래스에 의존하고 있기 때문에 뜻하지않은 에러가 발생한다. 코틀린에서는 이와같은 상속으로 인한 종속성, 의존성 문제를 방지하기 위해 기본적으로 클래스가 final이다. 또한 문제를 해결하기 위한 대안으로 Delegation pattern을 지원한다. 

하나의 클래스를 다른 클래스에 Delegation 하고 Delegation된 클래스가 가지는 인터페이스 메서드를 별도의 참조 없이 호출할 수 있도록 해준다는 것인데 이러한 Delegation을 by키워드를 사용하여 구현한다. 

 

  • 특정 처리를 다른 객체에게 넘기는 것을 의미함.
  • 다른 객체는 클래스 내부(포함)에 가지고 있음.

ViewModel

이번엔 viewModel 을 알아보도록 하자. 

  @VisibleForTesting val viewModel: MainViewModel by viewModels()

@VisibleForTesting

private으로 선언시 테스트코드에서 호출할 수 없기때문에 private 키워드를 제외하였을 때 다른 코드에서 접근할 수 없도록 해당 annotation을 추가하여 테스트 코드가 아닌 다른 곳에서 호출할 수 없다는 것을 나타냄 

 

위 코드는 공식문서에서 알려주고 있는 기본적인 viewModel() 생성방법이다. 

  override fun onCreate(savedInstanceState: Bundle?) {
    // onTransformationStartContainer()
    super.onCreate(savedInstanceState)
    binding.apply {
      lifecycleOwner = this@MainActivity
      //adapter = PokemonAdapter()
      vm = viewModel
    }
  }

viewModel과 dataBinding 생성이 완료되고 binding 객체에 lifecycleOwner와 viewModel을 연결한다. 

주석처리된 부분은 나중에 알아보도록 하겠다. 

 

이제 viewModel 로직을 알아보도록 하겠다. 

class MainViewModel @ViewModelInject constructor(
  private val mainRepository: MainRepository,
  @Assisted private val savedStateHandle: SavedStateHandle
) : LiveCoroutinesViewModel() {
	...
}

@ViewModelInjection

hilt의 annotation이다. 

별도의 java injection 이 아닌 hilt의 annotaion을 사용하면 조금 더 편하게 viewModel에 대한 DI를 구현할 수있다. 

 

@Assisted private val savedStateHandle: SavedStateHandle

공식문서에 위 항목을 추가해야한다고 나와있다.

 

SavedStateHandle

  • Saved State Handle 정보가 ViewModel로 전달됨
  • SavedStateViewModelFactory를 사용해야만 ViewModel을 통해서 SvaedStateHandled을 전달 가능
  • SvaedStateHandled은 Key-Value로 이루어진 Map 형태
  • 시스템이 프로세스를 종료하더라도 동일한 정보를 유지
  • get(String) -> 값 읽기
  • getLiveData(String) -> MutableLiveData가 반환, LiveData를 통해 값을 사용 가능
  • set(String, Object) -> 값 쓰기

viewModel을 생성하면 Repository가 추가된다. 

내용이 너무 길어지기 때문에 다음장에서 repository를 추가한 후 개인 프로젝트에 적용하여 테스트해보도록 하겠다. 

 

 

 

참고자료 

 

developer.android.com/training/dependency-injection/hilt-android?hl=ko

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스와 종속 항목을 수동으로 구성

developer.android.com

codechacha.com/ko/kotlin-inline-functions/

 

Kotlin - inline functions 이해하기

inline functions는 함수 내용을 호출하는 부분에 복사하여 추가적인 메모리 할당이나 함수 호출로 발생하는 Runtime overhead를 줄여줍니다. noinline 키워드는 특정 인자만 제외하고 나머지만 inlnie으로

codechacha.com

sungjk.github.io/2019/09/07/kotlin-reified.html

 

코틀린에서 reified는 왜 쓸까?

런타임에 Type erasure 없이 제네릭 타입을 보존시켜주는 reified 키워드에 대한 알아보았습니다.

sungjk.github.io

medium.com/@joongwon/kotlin-kotlin-lazy-initialization-901079296e43

 

[Kotlin] Kotlin Lazy Initialization

오늘은 Lazy Initialization에 대해서 알아볼 것이다. Initialization는 초기화를 뜻하니 익숙하겠지만 Lazy에 대해선 익숙하지 않은 분들도 있을 것이다. Lazy의 사전적 의미는 다음과 같다.

medium.com

velog.io/@jojo_devstory/%EC%BD%94%ED%8B%80%EB%A6%B0-Kotlin-by-by-the-way-what-is-this

 

코틀린 (Kotlin) by - by the way, what is this?

코틀린은 다양한 키워드 및 연산자를 지원하며 Hard Keywords, Soft Keywords, Modifier Keywords, Operators로 공식 코틀린 문서에 분류하여 소개되어있습니다.그중에서 Soft Keywords의 하나인 by에 대해 정리해보

velog.io