ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Codelabs Tutorial] Android 앱에서 Hilt 사용해보기 (2)
    프로그래밍/Android 2022. 5. 10. 21:28

    https://zerodeg.tistory.com/60

     

    [Codelabs Tutorial] Android 앱에서 Hilt 사용해보기 (1)

    https://developer.android.com/codelabs/android-hilt#0 Android 앱에서 Hilt 사용  | Android 개발자  | Android Developers 이 Codelab에서는 Hilt를 사용하여 종속 항목 삽입을 실행하는 Android 앱을 빌드..

    zerodeg.tistory.com

    지난 시간에 이어서 Hilt Codelabs Tutorial을 마무리 해보겠습니다. 해당 글은 제 공부목적으로 작성하고 있는 글입니다 :)

    지난 포스팅에서 Hilt를 통해 LoggerLocalDataSource 인스턴스 제공 방법을 알고 있는 상태입니다. (혹시 무슨 이야기를 하는지 모르겠다면 위 링크를 타고 지난 포스팅을 먼저 보고 와주세요. ) 그러나 이번에는 Type에 전이 종속 (Transitive Dependencies) 항목이 있습니다. LoggerLocalDataSource에서 인스턴스를 제공하려면 Hilt에서 LogDao 인스턴스 제공 방법도 알아야 합니다. LogDao는 인터페이스이므로 생성자가 없기 떄문에 생성자에 @Inject를 달아줄 수 없습니다.

    이때 Module을 사용하여 Hilt에 결합을 추가해줄 수 있습니다. 즉, 모듈을 사용하여 Hilt의 다양한 Type의 인스턴스 제공 방법을 알려줍니다. 인터페이스나 프로젝트에 포함되지 않은 클래스와 같이 생성자가 삽입될 수 없는 유형의 결합을 Hilt모듈에 포함시킵니다. 빌더를 사용하여 인스턴스를 생성해야 하는 OkHttpClient를 한 예로 들 수 있습니다.

    Hilt에서 삽입할 수 있는 Android 클래스마다 연결된 Hilt구성요소가 있습니다. 예를 들어, Application Container는 ApplicationComponent와 연결되며 Fragment Container는 FragmetnComponent와 연결됩니다.

    Module 만들기

    결합을 추가할 수 있는 Hilt 모듈을 만들어보겠습니다. hilt 패키지 아래 di라는 새 패키지를 만들고 패키지 내에 DatabaseModule.kt라는 새 파일을 만듭니다. LoggerLocalDataSource는 어플리케이션 컨테이너로 범위가 지정되므로 어플리케이션 컨테이너에서 LogDao 결합을 사용할 수 있어야 합니다. 여기서는 어플리케이션 컨테이너에 연결된 Hilt구성요소 클래스(ApplicationCompoenet:class SingletonComponent::class 로 이름이 변경되었음)를 전달하여 @InstallIn Annotation으로 요구사항을 지정합니다.

    class ServiceLocator(applicationContext: Context) {
    
        private val logsDatabase = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    
        val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())
    
        fun provideDateFormatter() = DateFormatter()
    
        fun provideNavigator(activity: FragmentActivity): AppNavigator {
            return AppNavigatorImpl(activity)
        }
    }

    ServiceLocator 클래스 구현체를 보면 LogDao 인스턴스는 logsDatabase.logDao()를 호출하여 가져옵니다. 따라서, LogDao 인스턴스를 제공하기 위해 AppDatabase 클래스에 전이 종속 항목이 있습니다. Hilt 모듈에 있는 함수에 Provides Annotation을 달아 Hilt에 생성자가 삽입될 수 없는 유형의 제공방법을 알려 줄 수 있습니다.  @Provides Annotation이 있는 함수 본문은 Hilt에서 이 유형의 인스턴스를 제공해야할 때마다 실행됩니다. @Provides Annotation이 있는 함수의 반환 Type은 Hilt에 결합 Type 또는 Type의 인스턴스 제공 방법을 알려줍니다. 함수 매개변수는 Type의 종속 항목입니다. 이 경우 DatabaseModule 클래스에 이 함수가 포함됩니다.

    @InstallIn(SingletonComponent::class)
    @Module
    object DatabaseModule {
    
        @Provides
        @Singleton
        fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
            return Room.databaseBuilder(
                appContext,
                AppDatabase::class.java,
                "logging.db"
            ).build()
        }
    
        @Provides
        fun provideLogDao(database: AppDatabase): LogDao {
            return database.logDao()
        }
    
    }

    위의 코드는 Hilt에 LogDao 인스턴스를 제공할 때 database.logDao()가 실행되어야 한다고 알려줍니다. AppDatabase가 전이 종속 항목이므로 Hilt에 이 유형의 인스턴스 제공 방법도 알려줘야 합니다. AppDatabase는 Room에서 생성하지 않으므로 프로젝트에서 소유하지 않는 다른 클래스이기 때문에 ServiceLocator 클래스에서 데이터베이스 인스턴스를 빌드하는 방식과 비슷하게 @Privedes 함수를 사용하여 제공할 수도 있습니다.

    Hilt에서 항상 동일한 데이터베이스 인스턴스를 제공하도록 하려면 @Provides provideDatabase 메소드에 @Singleton Annotation을 추가합니다. 각 Hilt 컨테이너는 맞춤 결합에 종속 항목으로 삽입될 수 있는 일련의 기본 결합을 제공합니다. 이는 applicationContext의 사례로, 엑세스하려면 필드에 @ApplicationContext Annotation을 달아야 합니다. 

    이제 Hilt는 LogsFragment에 인스턴스를 삽입하는 데 필요한 모든 정보를 갖고 있습니다. 그러나, Hilt는 앱을 싱핼하기 전에 작동을 위해 Fragment를 호스팅하는 Activity를 알아야 합니다. MainActivity에 @ActivityEntryPoint로 주석을 달아야 합니다. ui/MainActivity.kt 파일을 열고 MainActivity에 @AndroidEntryPoint Annotation을 추가합니다.

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        private lateinit var navigator: AppNavigator
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            navigator = (applicationContext as LogApplication).serviceLocator.provideNavigator(this)
    
            if (savedInstanceState == null) {
                navigator.navigateTo(Screens.BUTTONS)
            }
        }
    
        override fun onBackPressed() {
            super.onBackPressed()
    
            if (supportFragmentManager.backStackEntryCount == 0) {
                finish()
            }
        }
    }

    여기까지 진행하면 수정하기 전과 같이 앱이 잘 작동하는 것을 확인할 수 있습니다. LogsFragment에 있는 logger와 dateFormatter에 대한 DI가 완료되었습니다. 이제 MainActivity에서 ServiceLocator 호출을 삭제하는 방법에 대해 알아봅니다.

    @Binds로 인터페이스 제공하기

    MainActivity는 providerNavigator(activity: FragmentActivity) 함수를 호출하여 ServiceLocator에서 AppNavigator 인스턴스를 가져옵니다. AppNavigator는 인터페이스이므로 생성자 삽입을 사용할 수 없습니다. 인터페이스에 사용할 구현을 Hilt에 알리려면 Hilt 모듈내 함수에 @Binds 주석을 사용하면 됩니다.

    @Binds Annotation (튜토리얼에는 Annotation을 주석이라고 표현하고 있는데 한국어로 주석이라고 하면 // 나 /* */ 와 같은 주석이 먼저 생각나므로 Annotation이라고 구분하는게 좋을 것 같습니다.) 은 반드시 추상 메소드에 달아야합니다.(이 메소드는 추상 메소드이므로 코드를 포함하지 않고 클래스도 추상화 되어야 함). 추상 함수의 반환 타입은 구현을 제공하려는 인터페이스(AppNavigator)입니다. 구현은 인터페이스 구현 타입(AppNavigatorImpl)으로 매개변수를 추가하여 지정됩니다.

    생각해보면 이전에 만들어놓은 DatabaseModule 클래스에 정보를 추가할 수 도 있을 것 같은데요. 새 모듈을 만들어야하는 몇가지 이유가 있습니다.

    • 효율적인 구성을 위해 모듈 이름은 제공하는 정보 타입을 전달해야 합니다. 예를 들어 DatabaseModule이라는 모듈에 탐색 Bind를 포함하는 것은 적절하지 않습니다.
    • DatabaseModule 모듈은 SingletonComponent(ApplicationComponent에서 이름이 바뀜)에 설치되므로 Singleton(Application)Container에서 결합을 사용할 수 있습니다. 새 탐색 정보(AppNavigator)에는 Activity의 특정 정보가 필요합니다.(AppNavigatorImpl 은 Activity를 종속 항목으로 여기기 때문). 따라서, SingletonContainer가 아닌 Activity 정보를 사용할 수 있는 Activity 컨테이너에 설치해야 합니다.
    • Hilt 모듈에는 비정적 결합 메소드와 추상 결합 메소드를 모두 포함할 수 없으므로 동일한 클래스에 @Binds와 @Provides Annotation을 배치하면 안됩니다.
    @InstallIn(ActivityComponent::class)
    @Module
    abstract class NavigationModule {
    
        @Binds
        abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
    
    }

    di 폴더에 NavigationModule.kt라는 새 파일을 만듭니다. 여기에서는 Module 및 InstallIn(ActivityComponent::class) Annotation이 달린 NavigationModule이라는 새 추상 클래스를 만들어봅니다. 모듈 내에서 AppNavigator의 Bind(결합)를 추가할 수 있씁니다. 이는 Hilt에 알려주고 있는 인터페이스를 반환하는 추상메소드(AppNavigator)이며 매개변수는 인터페이스의 구현(AppNavigationImpl)입니다.

    이제 AppNavigatorImpl 인스턴스 제공 방법을 Hilt에게 알려줘야합니다. 이 클래스처럼 생성자를 삽입할 수 있는 경우 생성자에 @Inject Annotation을 추가하기만 하면 됩니다. navigator/AppNavigatorImpl.kt 파일을 열고 아래와 같이 합니다. AppNavigatorImpl은 FragmentActivity에 종속됩니다. AppNavigator 인스턴스가 Activity컨테이너에 제공될 때 사전 정의된 결합으로 제공되므로 이미 FragmentActivity를 사용할 수 있습니다. (이 인스턴스는 NavigationModule이 ActivityComponent에 설치되어 있으므로 Fragment 컨테이너와 View 컨테이너에서도 사용할 수 있습니다.)

    Activity에서 Hilt 사용

    이제 Hilt에서 AppNavigator인스턴스를 삽입할 수 있는 모든 정보를 보유하고 있습니다. MainActivity.kt파일을 열고 다음 안내를 따릅니다.

    • Hilt에서 가져올 수 있도록 navigator필드에 @Inject Annotation을 추가합니다.
    • 접근제한자 private를 삭제합니다.
    • onCreate 메소드에서 navigator 초기화 코드를 삭제합니다.
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject lateinit var navigator: AppNavigator
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
    //        navigator = (applicationContext as LogApplication).serviceLocator.provideNavigator(this)
    
            if (savedInstanceState == null) {
                navigator.navigateTo(Screens.BUTTONS)
            }
        }
    
        
        ...
        
    }

    이제 앱을 실행해서 확인해봅니다.

    이제 ServiceLocator를 사용하는 클래스는 ButtonsFragment 뿐입니다. Hilt는 ButtonsFragment에 필요한 모든 타입의 제공방법을 이미 알고 있으므로 클래스에서 필드 삽입만 실행하면 됩니다. 이전에 했던 것처럼 클래스를 Hilt에서 삽입한 필드로 만들려면 다음과 같이 하면 됩니다.

    • ButtonsFragment에 @AndroidEntryPoint Annotation을 추가합니다.
    • logger와 navigator 필드의 접근제한자를 삭제하고 @Inject Annotation을 추가합니다.
    • 필드 초기화 코드를 삭제합니다.

    모두 수정하면 아래와 같이 됩니다.

    @AndroidEntryPoint
    class ButtonsFragment : Fragment() {
    
        @Inject lateinit var logger: LoggerLocalDataSource
        @Inject lateinit var navigator: AppNavigator
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            return inflater.inflate(R.layout.fragment_buttons, container, false)
        }
    
    //      더 이상 필요없는 초기화 코드
    //    override fun onAttach(context: Context) {
    //        super.onAttach(context)
    //
    //        populateFields(context)
    //    }
    //
    //    private fun populateFields(context: Context) {
    //        logger = (context.applicationContext as LogApplication).
    //            serviceLocator.loggerLocalDataSource
    //
    //        navigator = (context.applicationContext as LogApplication).
    //            serviceLocator.provideNavigator(requireActivity())
    //    }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            view.findViewById<Button>(R.id.button1).setOnClickListener {
                logger.addLog("Interaction with 'Button 1'")
            }
    
            view.findViewById<Button>(R.id.button2).setOnClickListener {
                logger.addLog("Interaction with 'Button 2'")
            }
    
            view.findViewById<Button>(R.id.button3).setOnClickListener {
                logger.addLog("Interaction with 'Button 3'")
            }
    
            view.findViewById<Button>(R.id.all_logs).setOnClickListener {
                navigator.navigateTo(Screens.LOGS)
            }
    
            view.findViewById<Button>(R.id.delete_logs).setOnClickListener {
                logger.removeLogs()
            }
        }
    }

    LoggerLocalDataSource 인스턴스는 타입의 범위가 Application(Singleton)Container로 지정되므로 LogsFragment에서 사용한 인스턴스와 동일하게 됩니다.  하지만, AppNavigator인스턴스는 관련된 Activity컨테이너로 범위를 지정하지 않았기 때문에 MainActivity 인스턴스와 다릅니다. (NavigationModule에서 Activity Component에 설치된 것 아닌가..? Application(Singleton)Container의 경우 Singleton으로 객체가 생성되기 때문에 동일한 객체에 접근하게 되지만 AppNavigator는 그렇지 않기 때문에 MainActivity에 있는 ServiceLocator와는 다르게 된다.)

    이 시점에서 ServiceLocator 클래스는 더이상 종속 항목을 제공하지 않으므로 프로젝트에서 완전히 삭제해도 됩니다. 이 클래스의 사용처는 유일하게 LogsApplication에 남아있습니다. 하지만 더이상 필요하지 않으므로 삭제하도록 합니다.

    @HiltAndroidApp
    class LogApplication : Application() {
    
    //    lateinit var serviceLocator: ServiceLocator
    //
    //    override fun onCreate() {
    //        super.onCreate()
    //        serviceLocator = ServiceLocator(applicationContext)
    //    }
    }

    이렇게 해서 LogApplication 클래스에는 구현부가 남지 않게 되었습니다. 지금까지 배운 내용을 토대로 충분히 Android 앱에서 DI 도구로 Hilt를 사용할 수 있게 되었습니다. 

    댓글

Designed by Tistory.