Dagger 앱을 Hilt로 이전하기

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

이 Codelab에서는 Android 앱에서 종속 항목 삽입(DI)을 위해 Dagger를 Hilt로 이전하는 방법을 배웁니다. Android 앱에서 Dagger 사용 Codelab을 Hilt로 이전합니다. 이 Codelab에서는 이전을 계획하는 방법과 각 Dagger 구성요소를 Hilt로 이전하는 동안 앱이 작동하도록 함으로써 이전 중에 Dagger와 Hilt를 동시에 작동시키는 방법을 설명합니다.

종속 항목 삽입을 사용하면 코드 재사용성, 리팩터링 편의성, 테스트 편의성 측면에서 유용합니다. Hilt는 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리 Dagger를 기반으로 빌드되었습니다.

많은 Android 프레임워크 클래스가 OS 자체에 의해 인스턴스화되기 때문에, Android 앱에서 Dagger를 사용할 때 관련 상용구가 있습니다. Hilt는 다음 항목을 자동으로 생성하고 제공하여 대부분의 상용구를 삭제합니다.

  • Dagger와 Android 프레임워크 클래스를 통합하기 위한 구성요소(자동 생성되지 않으면 직접 생성해야 함)
  • Hilt에서 자동으로 생성하는 구성요소의 범위 주석
  • 사전 정의된 결합 및 한정자

무엇보다도 Dagger와 Hilt가 공존할 수 있으므로 필요에 따라 앱을 이전할 수 있습니다.

이 Codelab을 진행하는 동안 코드 버그, 문법 오류, 불명확한 문구 등의 문제가 발생하면 Codelab 왼쪽 하단에 있는 오류 신고 링크를 통해 문제를 신고해 주세요.

기본 요건

  • Kotlin 구문 사용 경험
  • Dagger 사용 경험

학습할 내용

  • Android 앱에 Hilt를 추가하는 방법
  • 이전 전략을 계획하는 방법
  • 구성요소를 Hilt로 이전하고 기존 Dagger 코드가 계속 작동하게 하는 방법
  • 범위가 지정된 구성요소를 이전하는 방법
  • Hilt를 사용하여 앱을 테스트하는 방법

필요한 항목

  • Android 스튜디오 4.0 이상

코드 가져오기

GitHub에서 Codelab 코드를 가져옵니다.

$ git clone https://github.com/googlecodelabs/android-dagger-to-hilt

또는 저장소를 ZIP 파일로 다운로드할 수 있습니다.

ZIP 파일 다운로드

Android 스튜디오 열기

Android 스튜디오를 다운로드해야 하는 경우 여기에서 다운로드하세요.

프로젝트 설정

이 프로젝트는 여러 GitHub 분기에 빌드됩니다.

  • master는 개발자가 확인하거나 다운로드한 분기로 Codelab의 시작점입니다.
  • interop은 Dagger 및 Hint 상호 운용성 분기입니다.
  • solution에는 테스트 및 ViewModel을 비롯해 이 Codelab의 솔루션이 포함되어 있습니다.

master 분기를 시작으로 자신의 속도에 맞게 Codelab 단계를 따르는 것이 좋습니다.

Codelab 중에 프로젝트에 추가해야 하는 코드 스니펫이 생길 것입니다. 코드 스니펫의 댓글에 명시된 코드를 삭제해야 하는 경우도 있을 수 있습니다.

특정 단계에 도움이 필요할 경우 중간 분기를 체크포인트로 활용하세요.

git을 사용하여 solution 분기를 가져오려면 다음 명령어를 사용하세요.

$ git clone -b solution https://github.com/googlecodelabs/android-dagger-to-hilt

또는 다음 위치에서 솔루션 코드를 다운로드하세요.

최종 코드 다운로드

자주 묻는 질문(FAQ)

샘플 앱 실행

먼저 시작 샘플 앱이 어떤 모습인지 살펴보겠습니다. 다음 안내에 따라 Android 스튜디오에서 샘플 앱을 엽니다.

  • ZIP 보관 파일을 다운로드한 경우 로컬에서 파일을 압축 해제합니다.
  • Android 스튜디오에서 프로젝트를 엽니다.
  • Run execute.png 버튼을 클릭하고 에뮬레이터를 선택하거나 Android 기기를 연결합니다. 등록 화면이 표시됩니다.

54d4e2a9bf8177c1.gif

앱은 Dagger를 지원하는 4가지 절차(활동으로 구현됨)로 구성됩니다.

  • 등록: 사용자는 사용자 이름과 비밀번호를 입력하고 이용약관에 동의하여 등록할 수 있습니다.
  • 로그인: 사용자는 등록 절차 중에 추가된 사용자 인증 정보를 사용하여 로그인하며 앱에서 등록 취소할 수도 있습니다.
  • : 사용자는 환영 인사를 받고 읽지 않은 알림 수를 확인할 수 있습니다.
  • 설정: 사용자는 로그아웃하고 읽지 않은 알림 수를 새로고침할 수 있습니다(임의 개수의 알림 생성).

프로젝트는 뷰의 모든 복잡성이 ViewModel로 이어지는 일반적인 MVVM 패턴을 따릅니다. 잠시 시간을 내어 프로젝트 구조를 숙지하세요.

8ecf1f9088eb2bb6.png

화살표는 객체 간의 종속 관계를 나타냅니다. 이를 애플리케이션 그래프라고 하며, 앱의 모든 클래스와 이들 간의 종속 관계를 보여줍니다.

master 분기 내 코드는 Dagger를 사용하여 종속 항목을 삽입합니다. 구성요소를 직접 작성하는 대신 Hilt를 사용하여 구성요소 및 기타 Dagger 관련 코드를 생성하도록 앱을 리팩터링합니다.

Dagger는 다음 다이어그램과 같이 앱에서 설정됩니다. 특정 유형에 있는 점은 유형의 범위가 유형을 제공하는 구성요소로 지정되었음을 의미합니다.

a1b8656d7fc17b7d.png

작업을 단순화하기 위해 처음에 다운로드한 master 분기를 통해 Hilt 종속 항목이 이 프로젝트에 이미 추가되었습니다. 다음 코드는 이미 프로젝트에 추가되었으므로 추가할 필요가 없습니다. 그렇지만 Android 앱에서 Hilt를 사용하기 위해 무엇이 필요한지 살펴보겠습니다.

라이브러리 종속 항목과 별개로 Hilt는 프로젝트에서 구성된 Gradle 플러그인을 사용합니다. 루트(프로젝트 수준) build.gradle 파일을 열고 클래스 경로에서 다음 Hilt 종속 항목을 찾습니다.

buildscript {
    ...
    ext.hilt_version = '2.28-alpha'
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

app/build.gradle을 열고 상단의 kotlin-kapt 플러그인 바로 아래에 있는 Hilt Gradle 플러그인 선언을 확인합니다.

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

마지막으로 Hilt 종속 항목과 주석 프로세서가 동일한 app/build.gradle 파일의 프로젝트에 포함되어 있습니다.

...
dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

Hilt를 포함한 모든 라이브러리는 프로젝트를 빌드하고 동기화할 때 다운로드됩니다. 이제 Hilt를 사용해 보겠습니다.

모든 데이터를 한 번에 Hilt로 이전하려고 할 수도 있지만, 실제 프로젝트에서는 Hilt로 단계별로 이전하는 동안 오류 없이 앱을 빌드하고 실행하길 원할 것입니다.

Hilt로 이전할 때 작업을 단계별로 정리하고자 한다면 애플리케이션 또는 @Singleton 구성요소를 이전하는 데서 시작하여 나중에 활동 및 프래그먼트를 이전하는 것이 좋습니다.

Codelab에서는 먼저 AppComponent를 이전한 다음 앱의 각 절차('등록 단계', '로그인', '기본 및 설정'의 순서로)를 이전합니다.

이전하는 동안 모든 @Component@Subcomponent 인터페이스를 삭제하고 @InstallIn으로 모든 모듈에 주석을 추가합니다.

이전 후 모든 Application/Activity/Fragment/View/Service/BroadcastReceiver 클래스에는 @AndroidEntryPoint 주석이 있어야 하고 구성요소를 인스턴스화하거나 전파하는 모든 코드는 삭제되어야 합니다.

이전을 계획하려면 AppComponent.kt부터 시작하여 구성요소 계층 구조를 이해해야 합니다.

@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {

    // Factory to create instances of the AppComponent
    @Component.Factory
    interface Factory {
        // With @BindsInstance, the Context passed in will be available in the graph
        fun create(@BindsInstance context: Context): AppComponent
    }

    // Types that can be retrieved from the graph
    fun registrationComponent(): RegistrationComponent.Factory
    fun loginComponent(): LoginComponent.Factory
    fun userManager(): UserManager
}

AppComponent에는 @Component 주석이 추가되고 StorageModule, AppSubcomponents 두 가지 모듈이 포함됩니다.

AppSubcomponents에는 RegistrationComponent, LoginComponent, UserComponent의 세 가지 구성요소가 있습니다.

  • LoginComponentLoginActivity에 삽입됩니다.
  • RegistrationComponentRegistrationActivity, EnterDetailsFragment, TermsAndConditionsFragment에 삽입됩니다. 또한 이 구성요소의 범위는 RegistrationActivity입니다.

UserComponentMainActivitySettingsActivity에 삽입됩니다.

ApplicationComponent 참조는 앱에서 이전하는 구성요소에 매핑되는 Hilt 생성 구성요소(생성된 모든 구성요소로 연결되는 링크)로 대체할 수 있습니다.

이 섹션에서는 AppComponent를 이전합니다. 기존 Dagger 코드가 계속 작동하도록 하려면 몇 가지 기본 작업을 해야 합니다. 그동안 다음 단계를 통해 각 구성요소를 Hilt로 이전합니다.

Hilt를 초기화하고 코드 생성을 시작하려면 Application 클래스에 Hilt 주석을 추가해야 합니다.

MyApplication.kt를 열고 클래스에 @HiltAndroidApp 주석을 추가합니다. 이러한 주석은 Dagger가 주석 프로세서에서 픽업하여 사용할 코드 생성을 트리거하도록 Hilt에 알립니다.

MyApplication.kt

package com.example.android.dagger

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

@HiltAndroidApp
open class MyApplication : Application() {

    // Instance of the AppComponent that will be used by all the Activities in the project
    val appComponent: AppComponent by lazy {
        initializeComponent()
    }

    open fun initializeComponent(): AppComponent {
        // Creates an instance of AppComponent using its Factory constructor
        // We pass the applicationContext that will be used as Context in the graph
        return DaggerAppComponent.factory().create(applicationContext)
    }
}

1. 구성요소 모듈 이전

시작하려면 AppComponent.kt를 엽니다. AppComponent에는 @Component 주석에 추가된 두 개의 모듈(StorageModuleAppSubcomponents)이 있습니다. 가장 먼저 해야 할 일은 Hilt에서 모듈 두 개를 생성된 ApplicationComponent에 추가하도록 두 모듈을 이전하는 것입니다.

이렇게 하려면 AppSubcomponents.kt를 열고 클래스에 @InstallInannotation 주석을 추가합니다. @InstallIn 주석은 매개변수를 사용하여 적절한 구성요소에 모듈을 추가합니다. 이 경우 애플리케이션 수준 구성요소를 이전할 때 ApplicationComponent에서 결합이 생성되기를 원할 수 있습니다.

AppSubcomponents.kt

// This module tells a Component which are its subcomponents
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        LoginComponent::class,
        UserComponent::class
    ]
)
class AppSubcomponents

StorageModule에서 동일하게 변경해야 합니다. StorageModule.kt를 열고 이전 단계에서 한 것처럼 @InstallIn 주석을 추가합니다.

StorageModule.kt

// Tells Dagger this is a Dagger module
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {

    // Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
    @Binds
    abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}

@InstallIn 주석을 통해 다시 한번 Hilt에서 생성된 ApplicationComponent에 모듈을 추가하도록 Hilt에 요청했습니다.

이제 뒤로 돌아가 AppComponent.kt를 확인해 보겠습니다. AppComponentRegistrationComponent, LoginComponent, UserManager의 종속 항목을 제공합니다. 다음 단계에서 이전을 위해 이러한 구성요소를 준비합니다.

2. 노출된 유형 이전

앱을 Hilt로 완전히 이전하는 동안 Hilt를 통해 진입점을 사용하여 Dagger의 종속 항목을 직접 요청할 수 있습니다. 진입점을 사용하면 모든 Dagger 구성요소를 이전하는 동안 앱이 계속 작동하도록 할 수 있습니다. 이 단계에서는 각 Dagger 구성요소를 Hilt에서 생성된 ApplicationComponent의 수동 종속 항목 조회로 교체합니다.

Hilt에서 생성된 ApplicationComponent에서 RegistrationActivity.kt를 위한 RegistrationComponent.Factory를 가져오려면 @InstallIn 주석이 추가된 새로운 EntryPoint 인터페이스를 만들어야 합니다. InstallIn 주석은 결합을 가져올 위치를 Hilt에 알립니다. 진입점에 액세스하려면 EntryPointAccessors의 적절한 정적 메서드를 사용하세요. 매개변수는 구성요소 인스턴스이거나 구성요소 소유자 역할을 하는 @AndroidEntryPoint 객체여야 합니다.

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface RegistrationEntryPoint {
        fun registrationComponent(): RegistrationComponent.Factory
    }

    ...
}

이제 Dagger 관련 코드를 RegistrationEntryPoint로 교체해야 합니다. RegistrationEntryPoint를 사용하도록 registrationComponent의 초기화를 변경합니다. 이번 변경으로 RegistrationActivity가 Hilt를 사용하도록 이전될 때까지 Hilt에서 생성된 코드를 통해 종속 항목에 액세스할 수 있습니다.

RegistrationActivity.kt

        // Creates an instance of Registration component by grabbing the factory from the app graph
        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, RegistrationEntryPoint::class.java)
        registrationComponent = entryPoint.registrationComponent().create()

다음으로, 노출된 다른 모든 유형의 구성요소에 대해 동일한 기본 작업을 해야 합니다. LoginComponent.Factory 작업을 계속 진행하겠습니다. LoginActivity를 열고 이전과 같이 @InstallIn@EntryPoint 주석이 추가된 LoginEntryPoint 인터페이스를 만들되 Hilt 구성요소에 필요한 LoginActivity를 노출합니다.

LoginActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface LoginEntryPoint {
        fun loginComponent(): LoginComponent.Factory
    }

Hilt가 LoginComponent를 제공하는 방법을 알았으니 이전 inject() 호출을 EntryPoint의 loginComponent()로 바꿉니다.

LoginActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
        entryPoint.loginComponent().create().inject(this)

AppComponent의 노출된 세 가지 유형 중 두 가지가 Hilt EntryPoint와 호환되도록 변경되었습니다. 그런 다음 UserManager를 비슷하게 변경해야 합니다. RegistrationComponent, LoginComponent와 달리 UserManagerMainActivitySettingsActivity 모두에서 사용됩니다. EntryPoint 인터페이스는 한 번만 생성하면 됩니다. 주석이 추가된 EntryPoint 인터페이스는 두 가지 활동 모두에서 사용될 수 있습니다. 간단히 하려면 MainActivity에서 인터페이스를 선언합니다.

UserManagerEntryPoint 인터페이스를 만들려면 MainActivity.kt를 열고 @InstallIn@EntryPoint를 주석으로 추가합니다.

MainActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface UserManagerEntryPoint {
        fun userManager(): UserManager
    }

이제 UserManagerEntryPoint를 사용하도록 UserManager를 변경합니다.

MainActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
        val userManager = entryPoint.userManager()

SettingsActivity.에서 동일하게 변경해야 합니다. SettingsActivity.kt를 열고 UserManager가 삽입된 방식을 변경합니다.

SettingsActivity.kt

    val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
    val userManager = entryPoint.userManager()

3. 구성요소 팩토리 삭제

@BindsInstance를 사용하여 Dagger 구성요소에 Context를 전달하는 것이 일반적인 패턴입니다. Context사전 정의된 결합으로 이미 사용 가능한 상태이므로 이는 Hilt에서 필요하지 않습니다.

리소스, 데이터베이스, 공유 환경설정 등에 액세스하려면 Context가 필요합니다. Hilt는 한정자 @ApplicationContext@ActivityContext를 사용하여 컨텍스트에 삽입하는 작업을 단순화합니다.

앱을 이전하는 동안 Context를 종속 항목으로 요구하는 유형을 확인하고 Hilt에서 제공하는 유형으로 교체합니다.

이 경우 SharedPreferencesStorage의 종속 항목으로 Context가 포함됩니다. 컨텍스트를 삽입하도록 Hilt에 알리려면 애플리케이션의 Context를 요구하는 SharedPreferencesStorage.kt. SharedPreferences를 열어 컨텍스트 매개변수에 @ApplicationContext 주석을 추가합니다.

SharedPreferencesStorage.kt

class SharedPreferencesStorage @Inject constructor(
    @ApplicationContext context: Context
) : Storage {

//...

4. 삽입 메서드 이전

그런 다음 inject() 메서드의 구성요소 코드를 확인하고 해당 클래스에 @AndroidEntryPoint 주석을 추가해야 합니다. 이 경우에는 AppComponentinject() 메서드가 없으므로 별도의 조치가 필요하지 않습니다.

5. AppComponent 클래스 삭제

AppComponent.kt에 나열된 모든 구성요소에 EntryPoint를 이미 추가했으므로 AppComponent.kt를 삭제할 수 있습니다.

6. 이전하는 데 구성요소를 사용하는 코드 삭제

더 이상 애플리케이션 클래스에서 맞춤 AppComponent를 초기화하는 데 코드가 필요하지 않습니다. 대신 Application 클래스가 Hilt에서 생성된 ApplicationComponent를 사용합니다. 클래스 본문 내의 모든 코드를 삭제합니다. 최종 코드는 아래의 코드 목록과 비슷해야 합니다.

MyApplication.kt

package com.example.android.dagger

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

@HiltAndroidApp
open class MyApplication : Application()

지금까지 Hilt를 애플리케이션에 추가했고 AppComponent를 삭제하고 Dagger 코드를 변경하여 Hilt에서 생성된 AppComponent를 통해 종속 항목을 삽입했습니다. 기기나 에뮬레이터에서 앱을 빌드하고 사용해 보면 앱이 이전과 똑같이 작동할 것입니다. 다음 섹션에서는 Hilt를 사용하도록 각 활동과 프래그먼트를 이전하겠습니다.

이제 애플리케이션 구성요소를 이전하고 기본 작업을 완료했으므로 각 구성요소를 하나씩 Hilt로 이전할 수 있습니다.

로그인 절차 이전을 시작하겠습니다. LoginComponent를 직접 만들어 LoginActivity에서 사용하는 대신 Hilt에서 자동으로 처리합니다.

이전 섹션에서 사용한 단계를 그대로 따를 수 있지만, 이번에는 Hilt에서 생성한 ActivityComponent를 사용하여 활동에서 관리하는 구성요소를 이전합니다.

LoginComponent.kt 열기에서 시작합니다. LoginComponent에는 모듈이 없으므로 별도의 조치가 필요하지 않습니다. Hilt가 LoginActivity의 구성요소를 생성하고 삽입하려면 활동에 @AndroidEntryPoint 주석을 추가해야 합니다.

LoginActivity.kt

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {

    //...
}

LoginActivity를 Hilt로 이전하기 위해 이 코드만 추가하면 됩니다. Hilt가 Dagger 관련 코드를 생성하므로 정리만 하면 됩니다. LoginEntryPoint 인터페이스를 삭제합니다.

LoginActivity.kt

    //Remove
    //@InstallIn(ApplicationComponent::class)
    //@EntryPoint
    //interface LoginEntryPoint {
    //    fun loginComponent(): LoginComponent.Factory
    //}

그런 다음 onCreate()에서 EntryPoint 코드를 삭제합니다.

LoginActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   //Remove
   //val entryPoint = EntryPoints.get(applicationContext, LoginActivity.LoginEntryPoint::class.java)
   //entryPoint.loginComponent().create().inject(this)

    super.onCreate(savedInstanceState)

    ...
}

Hilt에서 구성요소를 생성하기 때문에 LoginComponent.kt를 찾아서 삭제합니다.

LoginComponent는 현재 AppSubcomponents.kt에 하위 구성요소로 나열됩니다. Hilt에서 결합을 생성하므로 하위 구성요소 목록에서 LoginComponent를 안전하게 삭제할 수 있습니다.

AppSubcomponents.kt

// This module tells a Component which are its subcomponents
@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        UserComponent::class
    ]
)
class AppSubcomponents

Hilt를 사용하기 위해 LoginActivity를 이전할 때 위 코드만 있으면 됩니다. 이 섹션에서는 추가한 코드보다 훨씬 많은 코드를 삭제했습니다. Hilt를 사용하면 입력해야 하는 코드가 줄어들 뿐 아니라 버그를 발생시키는 코드 및 유지할 코드도 적어집니다.

이 섹션에서는 등록 절차를 이전합니다. 이전을 계획하기 위해 RegistrationComponent를 살펴보겠습니다. RegistrationComponent.kt를 열고 inject() 함수까지 아래로 스크롤합니다. RegistrationComponentRegistrationActivity, EnterDetailsFragmentTermsAndConditionsFragment에 종속 항목을 삽입합니다.

RegistrationActivity 이전을 시작하겠습니다. RegistrationActivity.kt를 열고 클래스에 @AndroidEntryPoint 주석을 추가합니다.

RegistrationActivity.kt

@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
    //...
}

이제 RegistrationActivity가 Hilt에 등록되어 있으므로 onCreate() 함수에서 RegistrationEntryPoint 인터페이스 및 EntryPoint 관련 코드를 삭제할 수 있습니다.

RegistrationActivity.kt

//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface RegistrationEntryPoint {
//    fun registrationComponent(): RegistrationComponent.Factory
//}

override fun onCreate(savedInstanceState: Bundle?) {
    //Remove
    //val entryPoint = EntryPoints.get(applicationContext, RegistrationEntryPoint::class.java)
    //registrationComponent = entryPoint.registrationComponent().create()

    registrationComponent.inject(this)
    super.onCreate(savedInstanceState)
    //..
}

Hilt는 삭제된 Dagger 구성요소에서 registrationComponent 변수 및 삽입 호출을 제거할 수 있도록 구성요소를 생성하고 종속 항목을 삽입합니다.

RegistrationActivity.kt

// Remove
// lateinit var registrationComponent: RegistrationComponent

override fun onCreate(savedInstanceState: Bundle?) {
    //Remove
    //registrationComponent.inject(this)
    super.onCreate(savedInstanceState)

    //..
}

그런 다음 EnterDetailsFragment.kt를 엽니다. RegistrationActivity에서 한 것과 비슷하게 EnterDetailsFragment@AndroidEntryPoint 주석을 추가합니다.

EnterDetailsFragment.kt

@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {

    //...
}

Hilt는 종속 항목을 제공하므로 삭제된 Dagger 구성요소에 대한 inject() 호출은 필요하지 않습니다. onAttach() 함수 삭제

다음 단계는 TermsAndConditionsFragment를 이전하는 것입니다. TermsAndConditionsFragment.kt를 열고 클래스에 주석을 추가하고 이전 단계에서 한 것처럼 onAttach() 함수를 삭제합니다. 최종 코드는 다음과 같습니다.

TermsAndConditionsFragment.kt

@AndroidEntryPoint
class TermsAndConditionsFragment : Fragment() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel

    //override fun onAttach(context: Context) {
    //    super.onAttach(context)
    //
    //    // Grabs the registrationComponent from the Activity and injects this Fragment
    //    (activity as RegistrationActivity).registrationComponent.inject(this)
    //}

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_terms_and_conditions, container, false)

        view.findViewById<Button>(R.id.next).setOnClickListener {
            registrationViewModel.acceptTCs()
            (activity as RegistrationActivity).onTermsAndConditionsAccepted()
        }

        return view
    }
}

이번 변경으로 RegistrationComponent에 나열된 모든 활동 및 프래그먼트를 이전했으므로 RegistrationComponent.kt를 삭제할 수 있습니다.

RegistrationComponent를 삭제한 후 AppSubcomponents의 하위 구성요소 목록에서 관련 참조를 삭제해야 합니다.

AppSubcomponents.kt

@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
    subcomponents = [
        UserComponent::class
    ]
)
class AppSubcomponents

등록 절차 이전을 완료하기까지 한 단계가 남았습니다. 등록 절차는 자체 범위 ActivityScope를 선언하고 사용합니다. 범위는 종속 항목의 수명 주기를 제어합니다. 이 경우 ActivityScope가 Dagger에 RegistrationActivity로 시작된 절차 내에서 동일한 RegistrationViewModel 인스턴스를 삽입하도록 알립니다. Hilt는 이를 지원할 수 있도록 기본 제공되는 수명 주기 범위를 제공합니다.

RegistrationViewModel을 열고 @ActivityScope 주석을 Hilt에서 제공하는 @ActivityScoped로 변경합니다.

RegistrationViewModel.kt

@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {

    //...
}

ActivityScope는 다른 곳에서는 사용되지 않으며 안전하게 ActivityScope.kt를 삭제할 수 있습니다.

이제 앱을 실행하고 등록 절차를 시도해 보세요. 현재 사용자 이름과 비밀번호를 사용하여 로그인하거나 새 계정으로 등록을 해제한 다시 등록하여 절차가 이전과 같이 작동하는지 확인할 수 있습니다.

현재 Dagger와 Hilt가 앱에서 함께 작동합니다. Hilt는 UserManager를 제외한 모든 종속 항목을 삽입합니다. 다음 섹션에서는 UserManager를 이전하여 Dagger에서 Hilt로 완전히 이전합니다.

지금까지 이 Codelab에서는 구성요소 UserComponent를 제외한 대부분의 샘플 앱을 Hilt로 이전했습니다. UserComponent에는 맞춤 범위 @LoggedUserScope 주석이 추가됩니다. 즉, UserComponent@LoggedUserScope 주석이 있는 클래스에 동일한 UserManager 인스턴스를 삽입합니다.

UserComponent는 수명 주기가 Android 클래스에 의해 관리되지 않으므로 사용 가능한 Hilt 구성요소에 매핑되지 않습니다. 생성된 Hilt 계층구조 중간에 맞춤 구성요소를 추가하는 작업은 지원되지 않으므로 다음 두 가지 옵션 중에서 선택할 수 있습니다.

  1. 현재 상태의 프로젝트에 Hilt와 Dagger가 함께 실행되도록 둡니다.
  2. 범위가 지정된 구성요소를 사용 가능한 가장 가까운 Hilt 구성요소(이 경우 ApplicationComponent)로 이전하고 필요한 경우 null 허용 여부를 사용합니다.

이전 단계에서 #1을 이미 달성했습니다. 이 단계에서는 #2의 안내에 따라 애플리케이션을 Hilt로 완전히 이전합니다. 하지만 실제 앱에서는 특정 사용 사례에 가장 적합한 방법을 자유롭게 선택할 수 있습니다.

이 단계에서 UserComponent는 Hilt의 ApplicationComponent 일부로 이전될 예정입니다. 구성요소에 모듈이 있는 경우 ApplicationComponent에도 설치해야 합니다.

UserComponent에서 범위가 지정된 유형은 UserDataRepository뿐이며 @LoggedUserScope 주석이 추가됩니다. UserComponent가 Hilt의 ApplicationComponent와 만나게 되므로 UserDataRepository에는 @Singleton 주석이 추가되며, 개발자는 사용자가 로그아웃할 때 이 로직을 null로 변경합니다.

UserManager에는 이미 @Singleton 주석이 추가되어 있습니다. 즉, 앱 전체에서 동일한 인스턴스를 제공하고, 조금만 변경하여 Hilt와 동일한 기능을 달성할 수 있습니다. UserManagerUserDataRepository의 작동 방식을 변경하는 것부터 시작해 보겠습니다. 먼저 몇 가지 기본 작업을 해야 합니다.

UserManager.kt를 열고 다음 변경사항을 적용합니다.

  • 더 이상 UserComponent의 인스턴스를 만들 필요가 없으므로 생성자에서 UserComponent.Factory 매개변수를 UserDataRepository로 바꿉니다. 그럼 대신 UserDataRepository가 종속 항목으로 포함됩니다.
  • Hilt는 구성요소 코드를 생성하므로 UserComponent 및 setter를 삭제합니다.
  • userComponent를 확인하지 않고 userRepository의 사용자 이름을 확인하도록 isUserLoggedIn() 함수를 변경합니다.
  • 사용자 이름을 userJustLoggedIn() 함수에 매개변수로 추가합니다.
  • userDataRepositoryuserName으로 initData를 호출하도록 userJustLoggedIn() 함수 본문을 변경합니다. 이전 중에 삭제하게 될 userComponent를 대신하게 됩니다.
  • registerUser()loginUser() 함수의 userJustLoggedIn() 호출에 username을 추가합니다.
  • logout() 함수에서 userComponent를 삭제하고 userDataRepository.cleanUp() 호출로 바꿉니다.

완료된 UserManager.kt의 최종 코드는 다음과 같습니다.

UserManager.kt

@Singleton
class UserManager @Inject constructor(
    private val storage: Storage,
    // Since UserManager will be in charge of managing the UserComponent lifecycle,
    // it needs to know how to create instances of it
    private val userDataRepository: UserDataRepository
) {

    val username: String
        get() = storage.getString(REGISTERED_USER)

    fun isUserLoggedIn() = userDataRepository.username != null

    fun isUserRegistered() = storage.getString(REGISTERED_USER).isNotEmpty()

    fun registerUser(username: String, password: String) {
        storage.setString(REGISTERED_USER, username)
        storage.setString("$username$PASSWORD_SUFFIX", password)
        userJustLoggedIn(username)
    }

    fun loginUser(username: String, password: String): Boolean {
        val registeredUser = this.username
        if (registeredUser != username) return false

        val registeredPassword = storage.getString("$username$PASSWORD_SUFFIX")
        if (registeredPassword != password) return false

        userJustLoggedIn(username)
        return true
    }

    fun logout() {
        userDataRepository.cleanUp()
    }

    fun unregister() {
        val username = storage.getString(REGISTERED_USER)
        storage.setString(REGISTERED_USER, "")
        storage.setString("$username$PASSWORD_SUFFIX", "")
        logout()
    }

    private fun userJustLoggedIn(username: String) {
        // When the user logs in, we create populate data in UserComponent
        userDataRepository.initData(username)
    }
}

이제 UserManager가 완료되었으므로 UserDataRepository에서 몇 가지 사항을 변경해야 합니다. UserDataRepository.kt를 열고 다음 변경사항을 적용합니다.

  • Hilt에서 관리하게 될 종속 항목이므로 @LoggedUserScope를 삭제합니다.
  • UserDataRepository는 이미 UserManager에 삽입되었으므로 종속 항목이 순환되지 않도록 방지하려면 UserDataRepository의 생성자에서 UserManager 매개변수를 삭제합니다.
  • unreadNotifications를 null 허용 여부로 변경하고 setter를 비공개로 설정합니다.
  • 새로운 null 허용 여부 변수인 username을 새로 추가하고 setter를 비공개로 설정합니다.
  • usernameunreadNotifications를 임의의 숫자로 설정하는 새로운 함수 initData()를 추가합니다.
  • 새 함수 cleanUp()을 추가하여 usernameunreadNotifications 수를 재설정합니다. username을 null로 설정하고 unreadNotifications를 -1로 설정합니다.
  • 마지막으로 randomInt() 함수를 클래스 본문 내부로 이동합니다.

완료된 최종 코드는 다음과 같습니다.

UserDataRepository.kt

@Singleton
class UserDataRepository @Inject constructor() {

    var username: String? = null
        private set

    var unreadNotifications: Int? = null
        private set

    init {
        unreadNotifications = randomInt()
    }

    fun refreshUnreadNotifications() {
        unreadNotifications = randomInt()
    }
    fun initData(username: String) {
        this.username = username
        unreadNotifications = randomInt()
    }

    fun cleanUp() {
        username = null
        unreadNotifications = -1
    }

    private fun randomInt(): Int {
        return Random.nextInt(until = 100)
    }
}

UserComponent 이전을 완료하려면 UserComponent.kt를 열고 inject() 메서드까지 아래로 스크롤합니다. 이 종속 항목은 MainActivitySettingsActivity에서 사용됩니다. MainActivity 이전을 시작하겠습니다. MainActivity.kt를 열고 클래스에 @AndroidEntryPoint 주석을 추가합니다.

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    //...
}

UserManagerEntryPoint 인터페이스를 삭제하고 onCreate()에서 진입점 관련 코드도 삭제합니다.

MainActivity.kt

//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface UserManagerEntryPoint {
//    fun userManager(): UserManager
//}

override fun onCreate(savedInstanceState: Bundle?) {
    //val entryPoint = EntryPoints.get(applicationContext, UserManagerEntryPoint::class.java)
    //val userManager = entryPoint.userManager()
    super.onCreate(savedInstanceState)

    //...
}

UserManagerlateinit var를 선언하고 Hilt에서 종속 항목을 삽입할 수 있도록 @Inject 주석을 추가합니다.

MainActivity.kt

@Inject
lateinit var userManager: UserManager

UserManager가 Hilt에 의해 삽입되므로 UserComponent에서 inject() 호출을 삭제합니다.

MainActivity.kt

        //Remove
        //userManager.userComponent!!.inject(this)
        setupViews()
    }
}

이러한 작업은 모두 MainActivity에 필요한 사항입니다. 이제 비슷하게 코드를 변경하여 SettingsActivity를 이전할 수 있습니다. SettingsActivity를 열고 @AndroidEntryPoint 주석을 추가합니다.

SettingsActivity.kt

@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
    //...
}

UserManager에 대한 lateinit var를 만들고 @Inject 주석을 추가합니다.

SettingsActivity.kt

    @Inject
    lateinit var userManager: UserManager

userComponent()에서 진입점 코드 및 삽입 호출을 삭제합니다. 완료된 onCreate() 함수는 다음과 같습니다.

SettingsActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        setupViews()
    }

이제 사용하지 않는 리소스를 삭제하여 이전을 완료할 수 있습니다. LoggedUserScope.kt, UserComponent.kt 및 마지막으로 AppSubcomponent.kt 클래스를 삭제합니다.

이제 앱을 실행한 다음 다시 시도해 보세요. 앱이 Dagger에서처럼 작동합니다.

Hilt로 앱 이전을 완료하기 전에 마쳐야 하는 중요한 단계가 하나 있습니다. 지금까지 모든 앱 코드를 이전했지만 테스트는 이전하지 않았습니다. Hilt는 앱 코드에서와 마찬가지로 테스트에 종속 항목을 삽입합니다. Hilt는 각 테스트의 새로운 구성요소 세트를 자동으로 생성하므로 Hilt를 사용한 테스트에는 유지보수가 필요하지 않습니다.

단위 테스트

단위 테스트부터 시작해 보겠습니다. 생성자에 주석이 추가되지 않은 것처럼 가짜 또는 모의 종속 항목을 전달하는 대상 클래스의 생성자를 직접 호출할 수 있으므로 단위 테스트에는 Hilt를 사용할 필요가 없습니다.

단위 테스트를 실행하면 UserManagerTest에 실패했다는 메시지가 표시됩니다. 이전 섹션의 생성자 매개변수를 포함하여 UserManager와 관련된 많은 작업과 변경 작업을 했습니다. UserComponentUserComponentFactory에 종속되는 UserManagerTest.kt를 엽니다. 이미 UserManager의 매개변수를 변경했으므로 UserComponent.Factory 매개변수를 새 UserDataRepository 인스턴스로 변경합니다.

UserManagerTest.kt

    @Before
    fun setup() {
        storage = FakeStorage()
        userManager = UserManager(storage, UserDataRepository())
    }

이제 됐습니다. 테스트를 다시 실행하면 모든 단위 테스트를 통과합니다.

테스트 종속 항목 추가

시작하기 전에 app/build.gradle을 열고 다음 Hilt 종속 항목이 있는지 확인합니다. Hilt는 테스트 관련 주석에 hilt-android-testing을 사용합니다. 또한 Hilt에서 androidTest 폴더의 클래스에 관한 코드를 생성해야 하므로 주석 프로세서도 이 위치에서 실행할 수 있어야 합니다.

app/build.gradle

    // Hilt testing dependencies
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

UI 테스트

Hilt는 각 테스트에 테스트 구성요소와 테스트 애플리케이션을 자동으로 생성합니다. 시작하려면 TestAppComponent.kt를 열어 이전을 계획합니다. TestAppComponent에는 모듈 2개(TestStorageModuleAppSubcomponents)가 있습니다. AppSubcomponents는 이미 이전하고 삭제했으므로 TestStorageModule 이전을 계속할 수 있습니다.

TestStorageModule.kt를 열고 @InstallIn 주석을 사용하여 클래스에 주석을 추가합니다.

TestStorageModule.kt

@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
    //...

모든 모듈 이전을 완료했으므로 계속해서 TestAppComponent를 삭제합니다.

다음으로 ApplicationTest에 Hilt를 추가해 보겠습니다. @HiltAndroidTest와 함께 Hilt를 사용하는 UI 테스트에 주석을 추가해야 합니다. 이 주석은 각 테스트에 관한 Hilt 구성요소 생성을 담당합니다.

ApplicationTest.kt를 열고 다음 주석을 추가합니다.

  • @HiltAndroidTest는 Hilt에 이 테스트의 구성요소를 생성하도록 알립니다.
  • @UninstallModules(StorageModule::class)는 Hilt에 테스트 중 TestStorageModule이 대신 삽입될 수 있도록 앱 코드에서 선언된 StorageModule을 제거하도록 알립니다.
  • 또한 ApplicationTestHiltAndroidRule을 추가해야 합니다. 이 테스트 규칙은 구성요소의 상태를 관리하며 테스트에서 삽입 작업을 하는 데 사용됩니다. 최종 코드는 다음과 같습니다.

ApplicationTest.kt

@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    //...

Hilt가 모든 계측 테스트에 새 Application을 생성하므로 UI 테스트 실행 시 Hilt에서 생성한 Application을 사용해야 합니다. 이를 위해 맞춤 테스트 실행기가 필요합니다.

Codelab 앱에는 이미 맞춤 테스트 실행기가 있습니다. MyCustomTestRunner.kt를 엽니다.

Hilt는 이미 HiltTestApplication.이라는 테스트에 사용할 수 있는 Application을 제공합니다. newApplication() 함수 본문에서 MyTestApplication::class.javaHiltTestApplication::class.java로 변경해야 합니다.

MyCustomTestRunner.kt

class MyCustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {

        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

이번 변경으로 이제 MyTestApplication.kt 파일을 안전하게 삭제할 수 있습니다. 이제 테스트를 실행해 보세요. 모든 테스트를 통과해야 합니다.

Hilt에는 WorkManager 및 ViewModel과 같은 다른 Jetpack 라이브러리의 클래스를 제공하기 위한 확장 프로그램이 포함되어 있습니다. Codelab 프로젝트의 ViewModel은 아키텍처 구성요소의 ViewModel에서 확장하지 않은 일반 클래스입니다. ViewModel을 위한 Hilt 지원을 추가하기 전에 앱의 ViewModel을 아키텍처 구성요소로 이전하겠습니다.

ViewModel과 통합하려면 Gradle 파일에 다음과 같은 추가 종속 항목을 추가해야 합니다. 이러한 종속 항목은 이미 추가되어 있습니다. 라이브러리와 달리 Hilt 주석 프로세서보다 우선적으로 작동하는 주석 프로세서를 추가해야 합니다.

// app/build.gradle file

...
dependencies {
  ...
  implementation "androidx.fragment:fragment-ktx:1.2.4"
  implementation 'androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack_version'
  kapt 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
  kaptAndroidTest 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
}

일반 클래스를 ViewModel로 이전하려면 ViewModel()을 확장해야 합니다.

MainViewModel.kt를 열고 : ViewModel()을 추가합니다. 이렇게만 해도 아키텍처 구성요소 ViewModel로 이전할 수 있지만 Hilt에 ViewModel의 인스턴스를 제공하는 방법도 알려야 합니다. 그렇게 하려면 ViewModel의 생성자에 @ViewModelInject 주석을 추가합니다. @Inject 주석을 @ViewModelInject로 바꿉니다.

MainViewModel.kt

class MainViewModel @ViewModelInject constructor(
    private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}

다음으로 LoginViewModel을 열고 동일한 변경 작업을 합니다. 최종 코드는 다음과 같습니다.

LoginViewModel.kt

class LoginViewModel @ViewModelInject constructor(
    private val userManager: UserManager
): ViewModel() {
//...
}

마찬가지로 RegistrationViewModel.kt를 열고 ViewModel()로 이전하고 Hilt 주석을 추가합니다. 확장 프로그램 메서드 viewModels()activityViewModels()를 사용하면 이 ViewModel의 범위를 제어할 수 있으므로 @ActivityScoped 주석이 필요하지 않습니다.

RegistrationViewModel.kt

class RegistrationViewModel @ViewModelInject constructor(
    val userManager: UserManager
) : ViewModel() {

동일한 방법으로 EnterDetailsViewModelSettingViewModel을 이전합니다. 두 클래스의 최종 코드는 다음과 같습니다.

EnterDetailsViewModel.kt

class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {

SettingViewModel.kt

class SettingsViewModel @ViewModelInject constructor(
     private val userDataRepository: UserDataRepository,
     private val userManager: UserManager
) : ViewModel() {

이제 모든 ViewModel이 아키텍처 구성요소 ViewModel로 이전되고 Hilt 주석이 추가되었으니 삽입 방법을 이전할 수 있습니다.

그런 다음 View 계층에서 ViewModel이 초기화되는 방식을 변경해야 합니다. ViewModel은 OS에서 생성되며 이때 by viewModels() 위임 함수가 사용됩니다.

MainActivity.kt를 열고 @Inject 주석을 Jetpack 확장 프로그램으로 변경합니다. lateinit를 삭제하고 varval로 변경하고 필드를 private로 표시해야 합니다.

MainActivity.kt

//    @Inject
//    lateinit var mainViewModel: MainViewModel
    private val mainViewModel: MainViewModel by viewModels()

마찬가지로 LoginActivity.kt를 열고 ViewModel을 획득하는 방법을 변경합니다.

LoginActivity.kt

//    @Inject
//    lateinit var loginViewModel: LoginViewModel
    private val loginViewModel: LoginViewModel by viewModels()

그런 다음 RegistrationActivity.kt를 열고 비슷하게 변경하여 registrationViewModel을 획득합니다.

RegistrationActivity.kt

//    @Inject
//    lateinit var registrationViewModel: RegistrationViewModel
    private val registrationViewModel: RegistrationViewModel by viewModels()

EnterDetailsFragment.kt를 엽니다. EnterDetailsViewModel을 획득하는 방법을 대체합니다.

EnterDetailsFragment.kt

    private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()

마찬가지로 registrationViewModel을 획득하는 방법을 대체하되 이번에는 viewModels(). 대신 activityViewModels() 위임 함수를 사용합니다. registrationViewModel이 삽입되면 Hilt에서 활동 수준 범위가 지정된 ViewModel을 삽입합니다.

EnterDetailsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

TermsAndConditionsFragment.kt를 열고 다시 한번 viewModels() 대신 activityViewModels() 확장 함수를 사용하여 registrationViewModel.을 획득합니다.

TermsAndConditionsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

마지막으로 SettingsActivity.kt를 열고 settingsViewModel 획득 방법을 이전합니다.

SettingsActivity.kt

    private val settingsViewModel: SettingsViewModel by viewModels()

이제 앱을 실행하고 모든 것이 정상적으로 작동하는지 확인합니다.

축하합니다. Hilt를 사용하기 위한 앱 이전을 마쳤습니다. 이전을 완료했을 뿐 아니라 Dagger 구성요소를 하나씩 이전하는 동안 애플리케이션이 계속 작동하도록 유지했습니다.

이 Codelab에서는 애플리케이션 구성요소를 시작하고 기존 Dagger 구성요소로 Hilt가 작동하도록 하기 위해 필요한 기본 작업을 빌드하는 방법을 배웠습니다. 그리고 활동과 프래그먼트에 Hilt 주석을 사용하고 Dagger 관련 코드를 삭제하여 각 Dagger 구성요소를 Hilt로 이전했습니다. 구성요소 이전을 완료할 때마다 앱은 정상적으로 작동했으며 아무런 문제 없이 기능을 제공했습니다. 또한 Hilt에서 제공한 @ActivityContext@ApplicationContext 주석을 사용하여 ContextApplicationContext 종속 항목을 이전했습니다. 기타 Android 구성요소를 이전했습니다. 마지막으로 테스트를 이전하여 Hilt로 이전을 완료했습니다.

추가 자료

앱을 Hilt로 이전하는 방법을 자세히 알아보려면 Hilt로 이전 도움말을 참고하세요. Dagger를 Hilt로 이전하는 작업 외에 dagger.android 앱의 이전에 관해서 알아볼 수도 있습니다.