이제 의존성을 주입해보도록 하겠습니다. 이 앱은 Android App Architecture 구조에 따라 의존성 주입이 단방향으로 이루어지게 설계되었습니다. AppModule의 의존성을 BookSearchRepositoryImpl에 주입하고, BookSearchRepository 인스턴스는 BookSearchViewModel에 주입합니다. 그리고 각 프래그먼트에 BookSearchViewModel 인스턴스가 주입됩니다.
우선은 AppModule에서 만든 Retrofit, Room, DataStore 의존성을 Repository에 주입합니다.
+ @Singleton
+ class BookSearchRepositoryImpl @Inject constructor(
private val db: BookSearchDatabase,
private val dataStore: DataStore<Preferences>,
+ private val api: BookSearchApi,
) : BookSearchRepository {
// Api
override fun searchBooksPaging(query: String, sort: String): Flow<PagingData<Book>> {
- val pagingSourceFactory = { BookSearchPagingSource(query, sort) }
+ val pagingSourceFactory = { BookSearchPagingSource(api, query, sort) }
return Pager(
config = PagingConfig(
pageSize = PAGING_SIZE,
enablePlaceholders = false,
maxSize = PAGING_SIZE * 3
),
pagingSourceFactory = pagingSourceFactory
).flow
}
}
class BookSearchPagingSource(
+ private val api: BookSearchApi,
private val query: String,
private val sort: String,
) : PagingSource<Int, Book>() { }
BookSearchRepositoryImpl에 @Singleton을 붙여서 의존성 주입 가능한 스코프로 지정해 주고 다음과 같이 @Inject constructor를 붙여주면 그 안의 객체들은 Hilt가 주입하게 됩니다. 전역 싱글톤으로 사용하던 Retrofit도 이젠 Hilt로 주입하고 BookSearchPagingSource도 Repository로부터 Retrofit 객체를 전달받도록 수정하였습니다.
다음은 ViewModel에 의존성을 주입합니다. @HiltViewModel을 붙여서 BookSearchViewModel을 의존성 주입 가능한 스코프로 만들고 @Inject constructor를 이용해 Module에 만들어 준 Repository와 WorkManager 객체를 주입합니다. SavedStateHandle은 Module 설정 없이도 자동으로 주입됩니다.
+ @HiltViewModel
+ class BookSearchViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
private val workManager: WorkManager,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() { }
다음은 Fragment에 ViewModel을 주입합니다. Delegate 패턴으로 ViewModel을 초기화하기 위해 Activity와 Fragment Dependency를 추가합니다.
// ViewModel delegate
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
Delegate 패턴을 사용하면 Factory를 사용하지 않고도 ViewModel을 생성할 수 있습니다. Delegate 생성에 대한 구체적인 설명은 Jetpack 아키텍처 이론의 ViewModel과 Lifecycle 기초 강의를 참조하시기 바랍니다.
모든 프래그먼트에 @AndroidEntryPoint를 붙여 의존성 주입 가능한 스코프로 만들어주고 by activityViewModels로 ViewModel을 생성합니다. 이제 Activity에서 ViewModel을 생성하던 작업을 이제 Hilt가 하게 되었습니다. 그러면 Fragment가 MainActivity로부터 ViewModel 객체를 받아오던 부분과 ViewModelFactory는 더이상 필요 없기 때문에 삭제합니다.
+ @AndroidEntryPoint
class BookFragment : Fragment() {
...
- lateinit var bookSearchViewModel: BookSearchViewModel
+ private val bookSearchViewModel by activityViewModels<BookSearchViewModel>()
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- bookSearchViewModel = (activity as MainActivity).bookSearchViewModel
-@Suppress("UNCHECKED_CAST")
-class BookSearchViewModelProviderFactory(
- private val bookSearchRepository: BookSearchRepository,
- private val workManager: WorkManager,
- owner: SavedStateRegistryOwner,
- defaultArgs: Bundle? = null,
-) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
- override fun <T : ViewModel?> create(
- key: String,
- modelClass: Class<T>,
- handle: SavedStateHandle,
- ): T {
- if (modelClass.isAssignableFrom(BookSearchViewModel::class.java)) {
- return BookSearchViewModel(bookSearchRepository, workManager, handle) as T
- }
- throw IllegalArgumentException("Viewmodel class not found")
- }
-}
Hilt 사용을 위해서 MainActivity에 @AndroidEntryPoint를 붙여주고, 의존성객체를 초기화하던 부분은 모두 필요없어졌으니 삭제합니다. 이제 빌드를 수행하면 정상적으로 앱이 동작하는 것을 확인할 수 있습니다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private lateinit var navController: NavController
- lateinit var bookSearchViewModel: BookSearchViewModel
- private val Context.dataStore by preferencesDataStore(DATASTORE_NAME)
- private val workManager = WorkManager.getInstance(application)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// setNavigation()
setJetpackNavigation()
- val database = BookSearchDatabase.getInstance(this)
- val bookSearchRepository = BookSearchRepositoryImpl(database, dataStore)
- val factory = BookSearchViewModelProviderFactory(bookSearchRepository, workManager, this)
- bookSearchViewModel = ViewModelProvider(this, factory)[BookSearchViewModel::class.java]
}
이번엔 WorkManager 내부에 의존성을 주입해 보도록 하겠습니다.
WorkManager는 서비스를 관리하는 클래스입니다. 그래서 그 내부에 의존성을 주입하는 방법은 다른 객체와는 다르게 Hilt extension을 사용해야 합니다. Dependency를 다음과 같이 추가합니다.
// Hilt extension
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
다음은 Worker 내부에 주입할 의존성을 정의합니다. 여기서는 캐시 최적화 결과를 반환하는 String 객체를 AppModule에 정의하겠습니다.
object AppModule {
+ @Singleton
+ @Provides
+ fun provideCacheDeleteResult(): String = "Cache has Deleted by Hilt"
}
다음은 Worker가 Hilt 의존성을 주입 받을 수 있는 모듈로 만들어줍니다.
+@HiltWorker
+class CacheDeleteWorker @AssistedInject constructor(
+ @Assisted context: Context,
+ @Assisted workerParams: WorkerParameters,
+ private val cacheDeleteResult: String,
) : Worker(context, workerParams) {
override fun doWork(): Result {
return try {
- Log.d("WorkManager", "Cache has successfully deleted")
+ Log.d("WorkManager", cacheDeleteResult)
Result.success()
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
}
}
}
@HiltWorker, @AssistedInject, @Assisted 를 써서 Worker가 Hilt 의존성을 주입 받을 수 있게 만들어줍니다. 그리고나서 provideCacheManageResult가 반환한 객체를 주입하고 사용하면 됩니다. 한 가지 제한사항으로 Worker는 SingletonComponent 안에 설치된 의존성만을 주입받을 수 있습니다.
이렇게 정의한 Worker 클래스는 HiltWorkerFactory를 통해 생성해야 합니다. BookSearchApplication이 Configuration.Provider를 구현하게 하고 getWorkManagerConfiguration을 다음과 같이 정의하면 됩니다.
@HiltAndroidApp
class BookSearchApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
}
기본적으로 WorkManager를 초기화하는 작업은 App Startup 라이브러리의 WorkManagerInitializer를 통해 앱이 시작될 때 자동으로 이루어지게 됩니다. 하지만 여기서는 WorkManager 초기화 방식을 커스텀하였기 때문에 WorkManagerInitializer가 자동으로 실행되지 않도록 AndroidManifest.xml에 다음과 같은 설정을 추가해야 합니다.
</activity>
+ <provider
+ android:name="androidx.startup.InitializationProvider"
+ android:authorities="${applicationId}.androidx-startup"
+ android:exported="false"
+ tools:node="merge">
+ <!-- If you are using androidx.startup to initialize other components -->
+ <meta-data
+ android:name="androidx.work.WorkManagerInitializer"
+ android:value="androidx.startup"
+ tools:node="remove" />
+ </provider>
</application>
이제 Cache 삭제기능을 동작시켜보면 Hilt에 의해 주입된 메시지가 표시되는 것을 확인할 수 있습니다.
이렇게 해서 Hilt로 객체를 주입하는 법에 대해 알아보았습니다.