관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (4) Hilt Dependency Injection 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 구글 공식 프로젝트 Sunflower 스터디 (4) Hilt Dependency Injection

막무가내막내 2021. 4. 19. 14:47
728x90

 

[2021-04-29 업데이트]

 

 

[출처 및 참고] 

github.com/android/sunflower

 

android/sunflower

A gardening app illustrating Android development best practices with Android Jetpack. - android/sunflower

github.com

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

 

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

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

developer.android.com

developer.android.com/training/dependency-injection

 

Android의 종속 항목 삽입  |  Android 개발자  |  Android Developers

종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다. 종속 항목 삽입을 구현하면

developer.android.com

hyperconnect.github.io/2020/07/28/android-dagger-hilt.html

 

Dagger Hilt로 안드로이드 의존성 주입 시작하기

Dagger Hilt에 대해 알아보고 안드로이드 프로젝트에 적용하는 방법을 소개합니다.

hyperconnect.github.io

www.charlezz.com/wordpress/wp-content/uploads/2020/09/www.charlezz.com-2020-hilt-hilt-for-droidknights.pdf

 

 


[Hilt 개요]

현재 안드로이드에서 대표적인 DI 라이브러리는 Dagger2, Hilt, Koin이 있다.

Sunflower 프로젝트에서 DI는 가장 최신인 Hilt로 구현되어 있다.

Dagger2, Koin은 사용해본 경험이 있지만 Hilt는 처음 접해본다.  공식문서 위주로 참고하며 학습했다.

2021년 4월 24일 기준 한글과 영문원본 공식문서 내용 차이가 꽤 있다. 영문 문서를 되도록 읽도록 하자.

 

 

Hilt 란?

 

Hilt는 DI를 쉽게하기 위해 만들어진 안드로이드 라이브러리이다. Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 사용하는 표준 방법을 제공합니다. Hilt는 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및 Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리 Dagger를 기반으로 빌드되었습니다. 

 

 

Hilt의 등장 이유 ?

 

 

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

 

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

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

developer.android.com

developer.android.com/training/dependency-injection/hilt-android(영문 원본 문서)

 

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

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

developer.android.com

 

 

DI의 필요성도 공식문서에 나와있어서 한번 쭉 읽어봤다. 단계별로 진화된 모습을 보여주면서 DI의 필요성에 대해 잘 설명해놓았다.

developer.android.com/training/dependency-injection

 

Android의 종속 항목 삽입  |  Android 개발자  |  Android Developers

종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다. 종속 항목 삽입을 구현하면

developer.android.com

 

추가로 공식문서뿐만 아니라 블로그와 드로이드나이츠2020 세미나 자료가 많은 도움이 되었다.  읽어보길 강추한다. !!!  모든면에서 다 도움이 되었지만 첫번째 하이퍼커넥트 개발블로그 링크는 컴포넌트 계층도, 두번째 드오리드 나이츠2020 세미나링크는 Dagger2와 Hilt를 비교해주고 전체적인 내용을 매우 쉽게 풀어써서 이해하는데 큰 도움이 되었다.

hyperconnect.github.io/2020/07/28/android-dagger-hilt.html

 

Dagger Hilt로 안드로이드 의존성 주입 시작하기

Dagger Hilt에 대해 알아보고 안드로이드 프로젝트에 적용하는 방법을 소개합니다.

hyperconnect.github.io

www.charlezz.com/wordpress/wp-content/uploads/2020/09/www.charlezz.com-2020-hilt-hilt-for-droidknights.pdf

 


 

[Hilt Dependency]

Sunflower에서 Hilt 관련 디펜던시는 다음과 같다. 

//프로젝트 builde.gradle
buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

//앱 builde.gradle
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
    
    //test
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$rootProject.hiltVersion"
    androidTestImplementation "com.google.dagger:hilt-android-testing:$rootProject.hiltVersion"
}

 

안드로이드 공식문서에서의 Dependency는 다음과 같다.

// 프로젝트 수준 builde.gradle
buildscript {
    ...
    ext.hilt_version = '2.33-beta'
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}


// 모듈 수준 build.gradle
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
Note: Projects that use both Hilt and data binding require Android Studio 4.0 or higher.
Hilt uses Java 8 features. To enable Java 8 in your project, add the following to the app/build.gradle file:


android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

 

+) Hilt는 AndroidX 라이브러리와 호환되어있다.

 

 

 


 

[Hilt + Application + @HiltAndroidApp]

 

Hilt를 사용하기 위해서는 Application 클래스를 반드시 @HiltAndroidApp 과 함께 만들어주고 manifest android:name 에 세팅을 해주어야한다.

 

Sunflower에서 Application 클래스를 살펴보면 다음과 같이 구현되어 있다.

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MainApplication : Application()

 

@HiltAndroidApp 이란 무엇인가?

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

 

Hilt DI의 조물주 같은 존재다. 

 

Hilt 관련 컴포넌트, 모듈 등 모든 코드 생성을 시작하는 어노테이션으로 DI 환경을 빌딩하는데 Base가 되고 컨테이너 역할을 하게된다. 컴파일 타임 시 표준 Hilt 컴포넌트 빌딩에 필요한 클래스들을 초기화를 해주기 때문에 Hilt를 사용하는 앱은 반드시 @HiltAndroidApp을 가진 Application 클래스를 manifest app에 포함시켜야한다. 

 

추가로 @HiltAndroidApp는 Application 객체의 수명 주기에 연결된 앱의 최상단 부모 컴포넌트이므로 이와 관련한 수명주기와 ApplicationContext 등 같은 종속 항목들을 하위(서브) 컴포넌트들에게 제공할 수 있다.

(추가 참고 : 컴포넌트들은 계층으로 이루어져 있으며 하위(서브) 컴포넌트는 상위 컴포넌트의 의존성에 접근할 수 있다. )

 

@Singleton
@Component(
    modules = [ViewModelModule::class, ViewModelFactoryModule::class, AppSubComponentsModule::class, RepositoryModule::class, LocalDataModule::class, RemoteDataModule::class]
)
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun mainComponent(): MainComponent.Factory

}
class MyApplication : Application() {
    val appComponent: AppComponent by lazy {
        //initializeComponent()
        DaggerAppComponent.factory().create(this)
    }
}

기존 Dagger2를 쓸때 이렇게 복잡하게 생성되던게 저 @HiltAndroidApp 하나로 해결되다니 매우 편리해진 것 같다. 뒤에서 다룰 @AndroidEntryPoint 도 그렇고 모든게 간편해졌다.

 

 

 

 


 

 

[Hilt + View + @AndroidEntryPoint + Component 계층 및 Scope]

 

SunFlower 는 Jetpack Navigation과 함께 하나의 액티비티로 구현되어 싱글액티비티 디자인(또는 SPA) 지향하고 있다.

따라서 GardenActivity 라는 하나의 액티비티만 존재한다.

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil.setContentView
import com.google.samples.apps.sunflower.databinding.ActivityGardenBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class GardenActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView<ActivityGardenBinding>(this, R.layout.activity_garden)
    }
}

 

 

@AndroidEntryPoint이란 무엇인가? (2021-04 기준 한글 문서는 최신 업데이트가 안되어있다.)

 

한https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#android-classes글문서
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#android-classes
영문문서

 

@AndroidEntryPoint는 @HiltAndroidApp 설정 후 사용 가능하며 @AndroidEntryPoint 어노테이션이 추가된 안드로이드 클래스에 DI 컨테이너를 추가 해준다.

 

안드로이드 클래스 중 @AndroidEntryPoint는

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver 

를 지원한다.

 

앞서 Application 클래스에 @HiltAndroidApp으로 애플리케이션 수준인 최상단 컴포넌트를 사용할 수 있게 되었으니 그 하위(서브) Android 클래스들에 @AndroidEntryPoint 를 설정함으로써 Application(상위) -> 안드로이드 클래스(하위, 밑 예시참고) 종속 항목(dependecies)를 제공할 수 있게 해준다. 추가로 프래그먼트에 @AndroidEntryPoint를 설정하려면 상위 개념에 해당하는 액티비티에도 @AndroidEntryPoint가 설정 되어 있어야한다. (밑 코드 참고)

@AndroidEntryPoint
class GalleryFragment : Fragment() {
@AndroidEntryPoint
class GardenFragment : Fragment() {
@AndroidEntryPoint
class HomeViewPagerFragment : Fragment() {
@AndroidEntryPoint
class PlantDetailFragment : Fragment() {
@AndroidEntryPoint
class PlantListFragment : Fragment() {

위에 공식문서들에 작성되어 있는 것처럼 (Android 클래스에 @AndroidEntryPoint로 주석을 지정하면 이 클래스에 종속된 Android 클래스에도 주석을 지정해야 합니다. 예를 들어 프래그먼트에 주석을 지정하면 이 프래그먼트를 사용하는 활동에도 주석을 지정해야 합니다.)  액티비티에 종속되어있는 Fragment 들도 모두 @AndroidEntryPoint 가 되어있음을 볼 수 있다.

 

 

 

 

 Hilt 에서는 밑 사진과 같이 안드로이드 클래스를 위한 표준화된 컴포넌트 세트와 스코프를 제공한다. 

Component hierarchy( 컴포넌트 계층구조)
표준화된 컴포넌트 세트 https://developer.android.com/training/dependency-injection/hilt-android#generated-components
컴포넌트 생명주기 https://developer.android.com/training/dependency-injection/hilt-android#component-lifetimes
컴포넌트 스코프 https://developer.android.com/training/dependency-injection/hilt-android#component-scopes

이렇게 @AndroidEntryPoint 로 설정된 클래스는 개별적인 Hilt 컴포넌트를 만들고 컴포넌트의 계층 구조(Component hierarchy) 에 설명된 대로 각 상위 클래스 에서 종속 항목을 제공받을 수 있게 된다.

위 사진처럼 표준화된 컴포넌트 세트와 스코프를 제공함을 볼 수 있다. 이것들의 각각의 개념은 hyperconnect.github.io/2020/07/28/android-dagger-hilt.html 에서 설명을 잘 해놨으니 참고하면 좋다. (해당 사이트 내용 중 하나만 설명하면, ActivityRetainedComponent와 ActivityComponent의 차이점은 둘다 Activity의 수명을 갖되 전자는 ViewModel 특성처럼 Activity의 configuration change(디바이스 화면전환 등) 시에는 파괴되지 않고 유지하되 후자는 Destory()와 함께 파괴된다.)

 

 

 

 

마지막으로 Dagger2 썻을때와 비교해봤다. (Dagger2 를 잘모르는 초짜지만 헤헷...) 

@Singleton
@Component(
    modules = [ViewModelModule::class, ViewModelFactoryModule::class, AppSubComponentsModule::class, RepositoryModule::class, LocalDataModule::class, RemoteDataModule::class]
)
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun mainComponent(): MainComponent.Factory

}
@Module(subcomponents = [MainComponent::class, CodeInputComponent::class])
class AppSubComponentsModule
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
// Scope annotation that the RegistrationComponent uses
// Classes annotated with @ActivityScope will have a unique instance in this Component
@ActivityScope
@Subcomponent
interface MainComponent {
    @Subcomponent.Factory
    interface Factory {
        fun create(): MainComponent
    }

    fun inject(mainActivity: MainActivity)
    fun inject(reservationInfoFragment: ReservationInfoFragment)
    fun inject(reservationSelectFragment: ReservationSelectFragment)
    fun inject(loginReservationFragment: LoginReservationFragment)
    fun inject(qrCodeReservationFragment: QrCodeReservationFragment)
    fun inject(topFragment: TopFragment)
    fun inject(dateSelectFragment: DateSelectFragment)
    fun inject(timeSelectFragment: TimeSelectFragment)
    fun inject(reservationFragment: ReservationFragment)
}
class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
    lateinit var mainComponent: MainComponent

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory
    lateinit var viewModel: ReservationSharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        mainComponent = (application as MyApplication).appComponent.mainComponent().create()
        mainComponent.inject(this)
        super.onCreate(savedInstanceState)
        initSharedViewModel()
        initTimber()
    }

이전 프로젝트에서 Dagger2 로 만든 DI 컨테이너 그래프와 코드인데 저렇게 하나하나 액티비티의 Component를 만들어주고 @ActivityScope 같이 Scope를 정해주면서 할게 많았는데 Hilt는 Dagger와 다르게 직접 인스턴스화 할 필요없이 간단해짐을 볼 수 있었다.

 

 

 


[Hilt + ViewModel + @HiltViewModel + @Inject constructor()]

 

 

 

Hilt가 적용된 ViewModel 이다.  Dagger2와 비교해서 @Inject constructor는 똑같이 사용하는데 ViewModel 전용의 @HiltViewModel 이라는게 생겼다.  Dagger2 에서는 ViewModel하고 엮을때 되게 복잡했었는데 간단해졌다고 느꼈다. @Inject construtor 와 @HiltViewModel 을 이어서 살펴보겠다.

@HiltViewModel
class GalleryViewModel @Inject constructor(
    private val repository: UnsplashRepository
) : ViewModel() {
@HiltViewModel
class GardenPlantingListViewModel @Inject internal constructor(
    gardenPlantingRepository: GardenPlantingRepository
) : ViewModel() {
/**
 * The ViewModel for [PlantListFragment].
 */
@HiltViewModel
class PlantListViewModel @Inject internal constructor(
    plantRepository: PlantRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
/**
 * The ViewModel used in [PlantDetailFragment].
 */
class PlantDetailViewModel @AssistedInject constructor(
    plantRepository: PlantRepository,
    private val gardenPlantingRepository: GardenPlantingRepository,
    @Assisted private val plantId: String
) : ViewModel() {

 

 

@Inject constructor이란?

생성자 삽입 방법으로 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려주게 된다. 제공하는 방법을 알려주니 당연히 해당 인스턴스를 제공하는 방법도 알아야하고 제공해주는 놈도 있을 것이다.

https://developer.android.com/training/dependency-injection/hilt-android#define-bindings

 

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

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

developer.android.com

 

 

@HiltViewModel 이란?

 

먼저 @HiltViewModel을 타고 들어가면 다음과 같다.

@HiltViewModel 어노테이션이 붙은 ViewModel은 HiltViewModelFactory에 의해 생성되고 @AndroidEntryPoint 어노테이션이 붙은 액티비티와 프래그먼트에서 기본 디폴트로 회수해오는게 가능해지게 한다.

또한 @HilteViewModel에서 @Inject 어노테이션이 붙은 생성자는 생성자 파라미터가 Hilt에 의해 주입받을 거라는 거라고 정의내리는 종속성을 갖게 해준다.

/**
 * Identifies a {@link androidx.lifecycle.ViewModel} for construction injection.
 *
 * <p>The {@code ViewModel} annotated with {@link HiltViewModel} will be available for creation by
 * the {@link dagger.hilt.android.lifecycle.HiltViewModelFactory} and can be retrieved by default in
 * an {@code Activity} or {@code Fragment} annotated with {@link
 * dagger.hilt.android.AndroidEntryPoint}. The {@code HiltViewModel} containing a constructor
 * annotated with {@link javax.inject.Inject} will have its dependencies defined in the constructor
 * parameters injected by Dagger's Hilt.
 *
 * <p>Example:
 *
 * <pre>
 * &#64;HiltViewModel
 * public class DonutViewModel extends ViewModel {
 *     &#64;Inject
 *     public DonutViewModel(SavedStateHandle handle, RecipeRepository repository) {
 *         // ...
 *     }
 * }
 * </pre>
 *
 * <pre>
 * &#64;AndroidEntryPoint
 * public class CookingActivity extends AppCompatActivity {
 *     public void onCreate(Bundle savedInstanceState) {
 *         DonutViewModel vm = new ViewModelProvider(this).get(DonutViewModel.class);
 *     }
 * }
 * </pre>
 *
 * <p>Exactly one constructor in the {@code ViewModel} must be annotated with {@code Inject}.
 *
 * <p>Only dependencies available in the {@link dagger.hilt.android.components.ViewModelComponent}
 * can be injected into the {@code ViewModel}.
 *
 * <p>
 *
 * @see dagger.hilt.android.components.ViewModelComponent
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface HiltViewModel {}

 

 

 

@HiltViewModel 은 2021년 4월 7일 기준 안드로이드 공식문서 한글판에는 적혀있는게 없고 영문 문서에만  안드로이드 클래스에 디펜던시 주입하는데 ViewModel에는 @HiltViewModel 을 사용한다고 나와있다. (ViewModel 용 의존성주입)

 

Hilt 2.31에서 나왔고 기존의 ViewModel은 @Assisted와 ViewModelInject 을 사용하여 DI를 했는데 이제는 @Inject와 @HiltViewModel 를 활용하면 된다.

 

@AndroidEntryPoint 어노테이션가 있는 액티비티나 프래그먼트에서 이 Hilt가 적용된 ViewModel 인스턴스를 얻으려면 ViewModelProvider 나 kt-extensions 인 by viewmodels() 을 사용하면 된다.

 

이러한 변경점 때문에 현재는 다음 ViewModel을 위한 Hilt 디펜던시를 추가해주지 않아도 된다고한다. 

androidx.hilt:hilt-lifecycle-viewmodel

[이전방식]

https://developer.android.com/training/dependency-injection/hilt-jetpack#viewmodels

 

Hilt 및 Jetpack 통합  |  Android 개발자  |  Android Developers

Hilt에는 다른 Jetpack 라이브러리의 클래스를 제공하기 위한 확장이 포함되어 있습니다. Hilt는 현재 다음 Jetpack 구성요소를 지원합니다. 이러한 통합을 활용하려면 Hilt 종속 항목을 추가해야 합니

developer.android.com

 

 

[현재방식]

https://developer.android.com/training/dependency-injection/hilt-android#android-classes

 

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

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

developer.android.com

https://developer.android.com/training/dependency-injection/hilt-jetpack#viewmodels

 

Hilt 및 Jetpack 통합  |  Android 개발자  |  Android Developers

Hilt에는 다른 Jetpack 라이브러리의 클래스를 제공하기 위한 확장이 포함되어 있습니다. Hilt는 현재 다음 Jetpack 구성요소를 지원합니다. 이러한 통합을 활용하려면 Hilt 종속 항목을 추가해야 합니

developer.android.com

 

추가적인 내용은 다음 블로그에서 잘 설명해주셔서 참고하면 좋을 것 같다.

proandroiddev.com/whats-new-in-hilt-and-dagger-2-31-c46b7abbc64a

 

What’s new in Hilt and Dagger 2.31

Hilt is a dependency injection tool that has been introduced by Google in the last year. Hilt makes our projects cleaner and reduces setup…

proandroiddev.com

two22.tistory.com/42

 

Dagger ( Hilt ) 2.31 - 변경된 ViewModel 주입

Hilt 2.31에서 ViewModel 주입 방법이 바뀌었다. 기존 코드 implementation "com.google.dagger:hilt-android:2.30.1-alpha" kapt "com.google.dagger:hilt-android-compiler:2.30.1-alpha" implementation 'andro..

two22.tistory.com

 

 

 

ViewModel 관련 Hilt에서 위 사항 말고도 다음과 같이 @AssistedInject , @Assisted, @AssistedFactory 라는 어노테이션도 있다.

@AssitedInjectDagger의 컴파일타임 안정성과 의존성 주입 이후 원하는 의존성을 얻을 수 있도록 도와준다. 쉽게 말해서 동적인 파라미터들과 함께 의존성 주입을 할 수 있다. (예를들어 SavedState 정보나 고유ID값 등, WorkManager에서도 @Assisted 을 사용한다. WorkerParameters와 context 의 동적인 전달을 받기 위해서이다. )

그리고 밑 코드를 보면 지금까지의 ViewModel 처럼 by viewModels()로 생성하는거와 다르게 @AssistedFactory를 사용한다는 것을 볼 수 있다. 이러한 @AssistedInejct, @Assisted 뷰모델은 @AssistedFactory를 사용하여 뷰모델의 @Assisted 생성자 파라미터를 주입해 줄 수 있다.

@AndroidEntryPoint
class PlantDetailFragment : Fragment() {

    private val args: PlantDetailFragmentArgs by navArgs()

    @Inject
    lateinit var plantDetailViewModelFactory: PlantDetailViewModelFactory

    private val plantDetailViewModel: PlantDetailViewModel by viewModels {
        PlantDetailViewModel.provideFactory(plantDetailViewModelFactory, args.plantId)
    }
class PlantDetailViewModel @AssistedInject constructor(
    plantRepository: PlantRepository,
    private val gardenPlantingRepository: GardenPlantingRepository,
    @Assisted private val plantId: String
) : ViewModel() {

    val isPlanted = gardenPlantingRepository.isPlanted(plantId).asLiveData()
    val plant = plantRepository.getPlant(plantId).asLiveData()

    fun addPlantToGarden() {
        viewModelScope.launch {
            gardenPlantingRepository.createGardenPlanting(plantId)
        }
    }

    fun hasValidUnsplashKey() = (BuildConfig.UNSPLASH_ACCESS_KEY != "null")

    companion object {
        fun provideFactory(
            assistedFactory: PlantDetailViewModelFactory,
            plantId: String
        ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return assistedFactory.create(plantId) as T
            }
        }
    }
}

@AssistedFactory
interface PlantDetailViewModelFactory {
    fun create(plantId: String): PlantDetailViewModel
}

이 또한 다음 링크에서 내용을 찾아볼 수 있다.

dagger.dev/dev-guide/assisted-injection.html

 

Assisted Injection

Assisted injection is a dependency injection (DI) pattern that is used to construct an object where some parameters may be provided by the DI framework and others must be passed in at creation time (a.k.a “assisted”) by the user. A factory is typically

dagger.dev

proandroiddev.com/whats-new-in-hilt-and-dagger-2-31-c46b7abbc64a

 

What’s new in Hilt and Dagger 2.31

Hilt is a dependency injection tool that has been introduced by Google in the last year. Hilt makes our projects cleaner and reduces setup…

proandroiddev.com

www.charlezz.com/?p=44184

 

Dagger를 돕는 AssitedInject 무엇인가? | 찰스의 안드로이드

Warning : 이 포스팅은 Dagger2에 대한 이해가 필요하며, Dagger2에 대해서는 다루지 않고 있습니다. Dagger2에 대한 내용은 Dagger2를 알아보자편을 참조해주세요. AssistedInject란 무엇인가? Square에서 만든

www.charlezz.com

 

 

 

 

 


[Hilt + Repository + @Module + @InstallIn + @Provide + Component 계층 및 Scope ]

 

 

이제 MVVM에서 Model을 담당하는 Repository Hilt 를 살펴보겠다.

Dagger2와 비슷하게 되어있다.

@Singleton 으로 설정하여 어디서나 동일한 객체를 제공하도록 해주게 만들었다. 그리고 매개변수 생성자 객체는 @Inject로 의존성 주입받게 되어있다.

 

싱글톤으로 하는 이유를 잘 설명해 놓은 문장과 사진이 있어 가져와봤다. (질문 답변 문장 출처 : programmar.tistory.com/51)

싱글톤인 경우 (출처: https://www.charlezz.com/wordpress/wp-content/uploads/2020/09/www.charlezz.com-2020-hilt-hilt-for-droidknights.pdf)
Singleton 아닌 경우

Q. 참고로 Repository는 왜 싱글턴 객체여야 하는가?

A. Repository는 네트워크 작업 혹은 데이터베이스 작업을 위해 만들어진 뷰와 뷰 모델과는 별개의 공간입니다. 만약 싱글턴이 아닌 단순 클래스라고 가정하면, 매번 네트워크 작업 혹은 데이터베이스 작업이 일어날 시 새로운 클래스 객체를 생성한다는 것은 매우 비효율적입니다. 만약 클래스 생성이 오래 걸린다고 가정하면, 네트워크 작업 및 데이터베이스 작업을 하기 위해 클래스 객체를 생성하는 것은 네트워크 처리, 데이터베이스 처리 시간에 더해져 매우 오래 걸릴 것입니다. 그래서 싱글턴 객체로 선언을 하여 항상 어디서든 준비되어 있도록 합니다.

package com.google.samples.apps.sunflower.data

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class GardenPlantingRepository @Inject constructor(
    private val gardenPlantingDao: GardenPlantingDao
) {

    suspend fun createGardenPlanting(plantId: String) {
        val gardenPlanting = GardenPlanting(plantId)
        gardenPlantingDao.insertGardenPlanting(gardenPlanting)
    }

    suspend fun removeGardenPlanting(gardenPlanting: GardenPlanting) {
        gardenPlantingDao.deleteGardenPlanting(gardenPlanting)
    }

    fun isPlanted(plantId: String) =
        gardenPlantingDao.isPlanted(plantId)

    fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens()
}
package com.google.samples.apps.sunflower.data

import javax.inject.Inject
import javax.inject.Singleton

/*@Singleton으로 어디서나 동일한 객체를 제공합니다*/
/**
 * Repository module for handling data operations.
 *
 * Collecting from the Flows in [PlantDao] is main-safe.  Room supports Coroutines and moves the
 * query execution off of the main thread.
 */
@Singleton
class PlantRepository @Inject constructor(private val plantDao: PlantDao) {

    fun getPlants() = plantDao.getPlants()

    fun getPlant(plantId: String) = plantDao.getPlant(plantId)

    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int) =
        plantDao.getPlantsWithGrowZoneNumber(growZoneNumber)
}

 

 

UnsplashRepository는 클래스 위에 @Singleton이 되어있지 않다. 이 Repository는 꽃들의 정보를 서버 API에서 가져오는 기능을 갖고있다. 그리고 UnSplashService 인터페이스는 NetworkModule에서 @Singleton으로 생성하게 되어있다. 

DI패키지(DatabaseModule, NetworkModule) 은 뒤에서 살펴보겠다.

package com.google.samples.apps.sunflower.data

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.google.samples.apps.sunflower.api.UnsplashService
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class UnsplashRepository @Inject constructor(private val service: UnsplashService) {

    fun getSearchResultStream(query: String): Flow<PagingData<UnsplashPhoto>> {
        return Pager(
            config = PagingConfig(enablePlaceholders = false, pageSize = NETWORK_PAGE_SIZE),
            pagingSourceFactory = { UnsplashPagingSource(service, query) }
        ).flow
    }

    companion object {
        private const val NETWORK_PAGE_SIZE = 25
    }
}
package com.google.samples.apps.sunflower.api

import com.google.samples.apps.sunflower.BuildConfig
import com.google.samples.apps.sunflower.data.UnsplashSearchResponse
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

/**
 * Used to connect to the Unsplash API to fetch photos
 */
interface UnsplashService {

    @GET("search/photos")
    suspend fun searchPhotos(
        @Query("query") query: String,
        @Query("page") page: Int,
        @Query("per_page") perPage: Int,
        @Query("client_id") clientId: String = BuildConfig.UNSPLASH_ACCESS_KEY
    ): UnsplashSearchResponse

    companion object {
        private const val BASE_URL = "https://api.unsplash.com/"

        fun create(): UnsplashService {
            val logger = HttpLoggingInterceptor().apply { level = Level.BASIC }

            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(UnsplashService::class.java)
        }
    }
}
package com.google.samples.apps.sunflower.di

import com.google.samples.apps.sunflower.api.UnsplashService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {

    @Singleton
    @Provides
    fun provideUnsplashService(): UnsplashService {
        return UnsplashService.create()
    }
}

 

 

 

 

 

 

 

DI 패키지의 Module 들에 대해 살펴보겠다.  @Module 이 달린 모듈 클래스 들이 모여있다.

https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#hilt-modules

 

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

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

developer.android.com

 

모듈의 설명과 함께  드로이드 나이츠 2020의 찰스개발자님의 www.charlezz.com/wordpress/wp-content/uploads/2020/09/www.charlezz.com-2020-hilt-hilt-for-droidknights.pdf 의 그림 자료만 먼저 봐도 이해가 훨씬 수월하다.

출처 : www.charlezz.com/wordpress/wp-content/uploads/2020/09/www.charlezz.com-2020-hilt-hilt-for-droidknights.pdf
@InstallIn을 액티비티 컴포넌트에 사용할 것이다.
프래그먼트 컴포넌트가 액티비티 컴포넌트의 하위컴포넌트라 Module을 같이 제공받을 수 있다.

 

Sunflower 코드를 살펴보도록 하자.

먼저 DatabaseModule 이다. Hilt Module 로 @Module 어노테이션이 붙어있다.

package com.google.samples.apps.sunflower.di

import android.content.Context
import com.google.samples.apps.sunflower.data.AppDatabase
import com.google.samples.apps.sunflower.data.GardenPlantingDao
import com.google.samples.apps.sunflower.data.PlantDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {

    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return AppDatabase.getInstance(context)
    }

    @Provides
    fun providePlantDao(appDatabase: AppDatabase): PlantDao {
        return appDatabase.plantDao()
    }

    @Provides
    fun provideGardenPlantingDao(appDatabase: AppDatabase): GardenPlantingDao {
        return appDatabase.gardenPlantingDao()
    }
}
import dagger.hilt.DefineComponent;
import javax.inject.Singleton;

/** A Hilt component for singleton bindings. */
@Singleton
@DefineComponent
public interface SingletonComponent {}

@InstallIn 어노테이션을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려준다. (= Hilt가 생성하는 DI컨테이너에 어떤 모듈을 사용할지 가리킨다)

SingletonComponent 라는 Hilt에서 제공하는 기본 컴포넌트이다.

이러한 Hilt Component  세트는 밑 사진의 링크에서 볼 수 있다. 참고로 현재시간 기준 이 컴포넌트는 업데이트 후에 생긴거라 아직 한글 문서에는 나와있지 않다. SingletonComponent 외 다양한 컴포넌트가 있고 세세한 생명주기도 설정할 수 있다. 문서에 들어가서 쭉 한번 읽어봐야한다.

 

SingletonComponent 로 하는 이유를 생각해보면 DB, 서버API 통신 객체는 여러 하나의 클래스에 종속되어 있는게 아닌 여러 Repository에서 사용할 수 있고, 어디서든 접근이 가능해야 하므로 Singleton 컴포넌트로 작성을 합니다. 그러한 이유로  DatabaseModule과 뒤에 이어서 볼 NetworkModule 은 InstallIn(SingletonComponent::class)를 통해 싱글턴 모듈임을 나타내도록 한다.

 

Application 에서 주입되고 앱이 시작부터 죽을때까지 말 그대로 Singleton 으로 처음 생성된 같은 객체가 주입될 것이다. 

https://developer.android.com/training/dependency-injection/hilt-android

 

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

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

developer.android.com

 

 

 

 

 

@Porivides 부분을 보겠다.

위와 같은 이유로 DatabaseModule 에서 @Provides 어노테이션을 사용하여 Room 데이터베이스 종속 클래스들의 인스턴스를 삽입한다.

 

 

두번째 모듈인 NetworkModule 이다. 서버와 통신할 API Interface를 UnSplashService의 compnaion object 싱글톤으로 되어있는 Retrofit2 빌더패턴으로 생성해주는 방식으로 객체를 주입시켜준다. 나머지 어노테이션은 DatabaseModule과 동일하다.  

UnSplashService.create() 부분을 여기서는 companion object에 해놨는데 Module @Provides로 따로 빼도 될 것 같다. Dagger2나 Koin을 했을때 난 그렇게 했었다. ( 위에 사진처럼 ㅇㅇ)

@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {

    @Singleton
    @Provides
    fun provideUnsplashService(): UnsplashService {
        return UnsplashService.create()
    }
}
interface UnsplashService {

    @GET("search/photos")
    suspend fun searchPhotos(
        @Query("query") query: String,
        @Query("page") page: Int,
        @Query("per_page") perPage: Int,
        @Query("client_id") clientId: String = BuildConfig.UNSPLASH_ACCESS_KEY
    ): UnsplashSearchResponse

    companion object {
        private const val BASE_URL = "https://api.unsplash.com/"

        fun create(): UnsplashService {
            val logger = HttpLoggingInterceptor().apply { level = Level.BASIC }

            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(UnsplashService::class.java)
        }
    }
}

 

 

 

 

이상 간략하게(?) 정리하며 학습해봤다.

 

이외에도  Sunflower 프로젝트엔 없던

 

1. @EntryPoint  :Hilt가 지원하지않는 클래스에서 의존성이 필요한 경우 사용함 (예:ContentProvider,DFM,Dagger를사용하지않는3rd-party라이브러리등) 

 

2. @DefineComponent : 표준Hilt컴포넌트이외에새로운컴포넌트를만드는것 커스텀컴포넌트를사용하면 복잡하고이해하기어려워지기때문에꼭 필요한경우만사용 

 

3. @HiltWorker : WorkManager with Hilt : https://developer.android.com/training/dependency-injection/hilt-jetpack#workmanager

 

등 다양한 Hilt 의 기능들은 안드로이드 공식문서나 www.charlezz.com/wordpress/wp-content/uploads/2020/09/www.charlezz.com-2020-hilt-hilt-for-droidknights.pdf 에서 확인할 수 있으니 찾아보면 좋고 따로 복습할 예정이다.

 

 

 

 

 


Dagger2의 러닝커브를 Hilt로 많이 낮춘게 느껴졌다. 확실히 Dagger2를 처음 접했을때보단 Components 만드는 작업도 없고 편리해진 것 같다. 하지만 아직 첫 학습이고 배울게 많다.

 

계속해서 따로 복습하고 영문 공식문서를 읽어보는데 시간을 투자하려고한다.

 

 

 

 

틀리거나 이상한 문맥은 알려주시면 감사하겠습니다 :)

댓글, 공감, 구독은 큰 힘이 됩니다. 감사합니다. !!!

728x90
Comments