googlecodelabs에서 hilt에 관한 예제를 원래 코드(main branch)와 Hilt를 사용한 코드(solution branch)를 살펴보겠습니다.
Hilt에 대한 내용은 이전 포스팅을 참고해주세요
UI는 왼쪽의 ButtonFragment와 오른쪽의 LogsFragement가 있습니다.
프로젝트 구조는 아래와 같습니다
solution에서 제시한 memory(LoggerInMemoryDataSource) 저장 방식과 ContentProvider는 제외했습니다.
기존
/* hilt/LogApplication.kt */
class LogApplication : Application() {
lateinit var serviceLocator: ServiceLocator
override fun onCreate() {
super.onCreate()
serviceLocator = ServiceLocator(applicationContext)
}
}
어플 사용 중 유지되어야 할 객체에 대해서 초기화합니다.
/* hilt/ServiceLocator.kt */
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)
}
}
DateFormatter는 Long형 timestamp를 String 형으로 바꿔주는 커스텀 클래스입니다.
LoggerLocalDataSource는 아래의 인터페이스를 구체화한 클래스입니다.
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
AppNavigatorImpl는 아래의 인터페이스를 구체화한 클래스입니다.
interface AppNavigator {
// Navigate to a given screen.
fun navigateTo(screen: Screens)
}
Hilt
/* hilt/LogApplication.kt */
@HiltAndroidApp
class LogApplication : Application()
Hilt Module로 관리하기에 Hilt를 사용하기 위한 @HiltAndroidApp
만 사용
/* hilt/di/LoggingDatabaseModule.kt */
@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())
를 @Binds
방식으로 interface의 구현체를 종속시킨다.
"logsDatabase.logDao()"인 파라미터를 넘겨주는 부분은 이후에 다룸
/* hilt/util/DateFormatter.kt */
class DateFormatter @Inject constructor() {
...
fun formatDate(timestamp: Long): String {
...
}
}
fun provideDateFormatter() = DateFormatter()
는 별도의 파라미터가 없고, 함수 호출 시 생성되기에 Scope를 지정하지 않고 정의
/* hilt/di/NavigationModule.kt */
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
fun provideNavigator(activity: FragmentActivity): AppNavigator { return AppNavigatorImpl(activity) }
또한 @Binds
방식으로 interface의 구현체를 종속시킨다.
"activity"인 파라미터는 이후에 다룸
기존
applicationContext를 접근해서 데이터를 가져옵니다.
/* hilt/ui/MainActivity.kt */
class MainActivity : AppCompatActivity() {
private lateinit var navigator: AppNavigator
override fun onCreate(savedInstanceState: Bundle?) {
...
navigator = (applicationContext as LogApplication).serviceLocator.provideNavigator(this)
}
...
/* hilt/ui/ButtonsFragment.kt */
class ButtonsFragment : Fragment() {
private lateinit var logger: LoggerLocalDataSource
private lateinit var navigator: AppNavigator
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())
}
...
/* hilt/ui/LogsFragment.kt */
class LogsFragment : Fragment() {
private lateinit var logger: LoggerLocalDataSource
private lateinit var dateFormatter: DateFormatter
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()
}
...
Hilt
Hilt에게서 주입 받기에 @inject
를 지정 후 바로 사용할 수 있습니다.
/* hilt/ui/MainActivity.kt */
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var navigator: AppNavigator
...
/* hilt/ui/LogsFragment.kt */
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
/* hilt/ui/ButtonsFragment.kt */
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
@Inject lateinit var navigator: AppNavigator
...
기존
/* hilt/navigator/AppNavigatorImpl.kt */
class AppNavigatorImpl(private val activity: FragmentActivity) : AppNavigator {
...
}
Hilt
/* hilt/navigator/AppNavigatorImpl.kt */
class AppNavigatorImpl @Inject constructor(private val activity: FragmentActivity) : AppNavigator {
...
}
activityContext의 경우는 @ActivityContext
어노테이션 없이 Hilt가 종속항목을 제공해줍니다.
기존
/* hilt/data/LoggerLocalDataSource.kt */
class LoggerLocalDataSource(private val logDao: LogDao) {
...
}
Hilt
/* hilt/data/LoggerLocalDataSource.kt */
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) : LoggerDataSource {
...
}
기존의 ServiceLocator에서 아래와 같이 초기화하여 제공했습니다.
private val logsDatabase = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"logging.db"
).build()
val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())
이처럼 Database 객체는 Application의 필드로, dao를 생성 후 제공하기 위해서
아래와 같이 Database Module을 생성합니다.
/* hilt/di/DatabaseModule.kt */
@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()
}
}
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao)
의 생성자를 Hilt로 주기 위해 @Provides
를 이용해 외부라이브러리(Room)을 생성 후 제공합니다.
Database 객체를 Singleton
component로 생성 하고 이를 아래 provideLogDao의 생성자로 제공합니다.
이후 provideLogDao의 반환이 LoggerLocalDataSource의 생성자로 들어가게 됩니다.
해당 예제와
https://github.com/googlecodelabs/android-hilt
제시한 solution 및 다양한 활용은 다음과 같습니다.
https://github.com/googlecodelabs/android-hilt/tree/solution