ABOUT ME

-

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

    https://developer.android.com/codelabs/android-hilt#0

     

    Android 앱에서 Hilt 사용  |  Android 개발자  |  Android Developers

    이 Codelab에서는 Hilt를 사용하여 종속 항목 삽입을 실행하는 Android 앱을 빌드해 보겠습니다.

    developer.android.com

    코드랩에서 제공하고 있는 학습을 따라하며 Hilt에 대해 공부를 하려고 에디터를 켜게 되었습니다. 의존성 주입이라는 것에 대해 책으로도 공부를 해보고 실습도 몇 번 해보았지만 이해하기가 쉽지 않았고 가장 근본있는 튜토리얼을 따라해보면서 이해를 해보고자 합니다. 

    DI를 사용하면 코드를 재사용할 수 있으며, 리팩토링이 편해지고, 테스트에 용이해지는 효과를 볼 수 있습니다. Hilt는 독보적인 안드로이드용 종속 항목 삽입 라이브러리로 프로젝트에서 수동 DI를 사용하는 상용구 코드를 줄여줍니다. Hilt는 앱에 DI를 삽입하는 표준방식으로, 프로젝트의 모든 Android 구성요소에 컨테이너를 제공하고 컨테이너의 수명주기를 자동으로 관리합니다.

    이 방식은 DI 라이브러리인 Dagger를 활용한 것입니다. 저는 "아키텍터를 알아야 앱 개발이 보인다" 라는 책과 저자의 블로그를 통해 Dagger를 일부 학습해보았는데 알아야하는 것이 너무 많고 바로 실무에 쓰기에는 진입장벽이 많이 높아보였습니다. 그런 단점을 극복한 것이 Hilt라고 생각하면 될 것 같습니다.

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

    코드랩에서 제공하는 베이스 코드를 가지고 옵니다.

    package com.example.android.hilt
    
    import android.app.Application
    
    class LogApplication : Application() {
    
        lateinit var serviceLocator: ServiceLocator
    
        override fun onCreate() {
            super.onCreate()
            serviceLocator = ServiceLocator(applicationContext)
        }
    }

    시작코드에는 ServiceLocator 클래스의 인스턴스가 있다는 것을 알 수 있습니다. 이 인스턴스는 요구되는 클래스에서 요청에 따라 가져온 종속 항목을 만들고 저장합니다 이 클래스는 앱이 소멸될 떄 함께 소멸되므로 앱의 수명 주기에 연결되는 종속 항목의 컨테이너라고 생각할 수 있습니다.

    컨테이너는 코드베이스에 종속 항목을 제공하는 클래스로, 다른 유형의 앱 인스턴스를 만드는 방법을 알고 있습니다. 이러한 인스턴스를 생성하고 수명주기를 관리하여 인스턴스를 제공하는 데 피룡한 종속 항목 그래프를 관리합니다. 컨테이너는 컨테이너에서 제공하는 유형의 인스턴스를 가져올 수 있는 메소드를 노출합니다. 이러한 메소드는 항상 다른 인스턴스 또는 동일한 인스턴스를 반환할 수 있습니다. 메소드에서 항상 동일한 인스턴스를 제공한다면 인스턴스 유형은 컨테이너로 범위가 지정됩니다.

    이제 ServiceLocator 클래스를 Hilt를 사용하여 대체하고, 새 기능을 추가해보겠습니다.

    @HiltAndroidApp
    class LogApplication : Application() {
    
        ...
    }

    @HiltAndroidApp은 종속 항목 삽입을 사용할 수 있는 앱의 기본 클래스가 포함된 Hilt코드 생성을 트리거합니다. 앱 컨테이너는 앱의 상위 컨테이너이므로 다른 컨테이너는 이 상위 컨테이너에서 제공하는 종속 항목에 엑세스 할 수 있습니다. 

    필요 시 클래스의 ServiceLocator에서 종속 항목을 가져오는 대신 Hilt를 사용하여 이러한 종속 항목을 제공합니다. 이제 이 클래스의 ServiceLocator 호출을 대체해보겠습니다. LogsFragment 파일을 엽니다. LogFragment는 OnAttach에서 필드를 채웁니다. ServiceLocator를 사용하여 직접 LoggerLocalDataSource와 DataFormatter인스턴스를 채우는 대신 Hilt를 사용하여 이러한 유형의 인스턴스를 생성하고 관리할 수 있습니다. LogsFragment에서 Hilt를 사용하려면 @AndroidEntryPoint Annotation을 추가해야합니다.

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    		
            ...
            
    }

    Android 클래스에 @AndroidEntryPoint 주석을 달면 Android 클래스 수명 주기를 따르는 종속 항목 컨테이너가 생성됩니다. Hilt는 현재 Android 유형 중 Application(@HiltAndroidApp), Activity, Fragment, View, Service, BroadcastReceiver를 지원합니다. 또한 Hilt는 FragmentActivity를 확장하는 활동 (예를 들어 AppCompatActivity), Andtoid플랫폼의 Fragment가 아닌 Jetpack라이브러리 Fragment를 확장하는 Fragment만 지원합니다.

    @AndroidEntryPoint를 사용하면 Hilt는 LogsFragment의 수명주기에 연결된 종속 항목 컨테이너를 생성하고 LogsFragment에 인스턴스를 삽입할 수 있습니다. @Inject 주석을 사용하면 Hilt에서 삽입하려는 다른 유형의 인스턴스(logger, dataFormatter)를 필드에 삽입하도록 할 수 있습니다.

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @Inject lateinit var logger: LoggerLocalDataSource
        @Inject lateinit var dateFormatter: DateFormatter
    
        private lateinit var recyclerView: RecyclerView
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            return inflater.inflate(R.layout.fragment_logs, container, false)
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view).apply {
                setHasFixedSize(true)
            }
        }
    
    //	  Inject를 했으므로 이제 이 메소드를 구현할 필요가 없다. logger와 dataFormatter 초기화 부분 삭제
    //    override fun onAttach(context: Context) {
    //        super.onAttach(context)
    //
    //        populateFields(context)
    //    }
    //
    //    private fun populateFields(context: Context) {
    //        logger = (context.applicationContext as LogApplication).serviceLocator.loggerLocalDataSource
    //        dateFormatter =
    //            (context.applicationContext as LogApplication).serviceLocator.provideDateFormatter()
    //    }
    
        override fun onResume() {
            super.onResume()
    
            logger.getAllLogs { logs ->
                recyclerView.adapter =
                    LogsViewAdapter(
                        logs,
                        dateFormatter
                    )
            }
        }
    }

    필드삽입을 실행하려면 Hilt에서 이러한 종속 항목의 인스턴스를 어떻게 제공하는지 알아야 합니다. 이 경우 Hilt는 LoggerLocalDataSource와 DataFormatter의 인스턴스 제공 방법을 알아야 합니다. 하지만 Hilt는 아직 이러한 인스턴스를 제공하는 방법을 알지 못합니다.

    @Inject로 Hilt에 종속 항목 제공 방법을 알려줄 수 있습니다. ServiceLocator를 열어서 구현방법을 확인해봅시다.

    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)
        }
    }

    providerDateFormatter를 호출하면 DataFormatter 항상 다른 인스턴스가 생성되는 것을 확인할 수 있습니다. 인스턴스 제공 방법을 Hilt에 알리려면 삽입하려는 클래스의 생성자에 @Inject를 추가하면 됩니다. DateFormatter를 수정해봅시다. Kotlin에서 생성자에 Annotation을 달려면 constructor 키워드가 필요합니다.

    class DateFormatter @Inject constructor() {
    
        @SuppressLint("SimpleDateFormat")
        private val formatter = SimpleDateFormat("d MMM yyyy HH:mm:ss")
    
        fun formatDate(timestamp: Long): String {
            return formatter.format(Date(timestamp))
        }
    }

    LoggerLocalDataSource도 같은 방식으로 완료합니다.

    class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
    
        private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
        private val mainThreadHandler by lazy {
            Handler(Looper.getMainLooper())
        }
    
        fun addLog(msg: String) {
            executorService.execute {
                logDao.insertAll(
                    Log(
                        msg,
                        System.currentTimeMillis()
                    )
                )
            }
        }
    
        fun getAllLogs(callback: (List<Log>) -> Unit) {
            executorService.execute {
                val logs = logDao.getAll()
                mainThreadHandler.post { callback(logs) }
            }
        }
    
        fun removeLogs() {
            executorService.execute {
                logDao.nukeTable()
            }
        }
    }

    서로 다른 유형의 인스턴스를 제공하는 방법에 관해 Hilt에서 알고 있는 정보를 결합이라고도 합니다.
    현재 Hilt에는 DateFormatter 인스턴스와 LoggerLovalDataSource 인스턴스를 제공하는 방법, 두 가지의 결합이 있다고 할 수 있습니다.

    ServiceLocator 클래스를 열어보면 public field로 LoggerLocalDataSource가 있는 것을 확인 할 수 있는데요. ServiceLocator는 호출될 때마다 항상 동일한 LoggerLocalDataSource 인스턴스를 반환한다는 뜻입니다. 이를 '인스턴스 범위를 컨테이너로 지정(scoping an instance to a container)이라고 합니다. Hilt에서 이를 어떻게 처리하는지 알아봅시다.

    Annotation을 사용하여 인스턴스의 범위를 컨테이너로 지정할 수 있습니다. Hilt는 수명주기가 다른 여러 컨테이너를 생성할 수 있으므로 이러한 컨테이너로 범위가 지정된 다양한 Annotation이 있습니다. 인스턴스 범위를 어플리케이션 컨테이너로 지정하는 Annotation은 @Singleton 입니다. 이 Annotation을 사용하면 Type이 다른 Type의 종속 항목으로 사용되는지 또는 삽입된 필드여야 하는 지와 관계없이 어플리케이션 컨테이너에서 항상 같은 인스턴스를 제공합니다.

    @Singleton // 어플리케이션 컨테이너에서 항상 동일한 LoggerLocalDataSource 인스턴스를 제공하도록 함
    class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
    
        ...
        
    }

    Activity 클래스에 연결된 모든 컨테이너에는 동일한 로직을 적용할 수 있습니다. 예를 들어 Activity 컨테이너에서 항상 특정 Type에 관해 동일한 인스턴스를 제공하게하려면 Type에 @ActivityScoped Annotation을 달면 됩니다.

    계층 구조의 상위 컨테이너에서 사용할 수 있는 결합은 계층구조의 하위 수준에서도 사용할 수 있씁니다. 따라서, 어플리케이션 컨테이너에서 LoggerLocalDataSource의 인스턴스를 사용할 수 있다면 Activity 컨테이너와 Fragment 컨테이너에서도 동일한 인스턴스를 사용할 수 있습니다.

    댓글

Designed by Tistory.