의존성 주입, 안드로이드에서의 의존성 주입, 그리고 안드로이드 의존성 주입 라이브러리인 Hilt에 대해서 지난번 포스팅에서 다뤘다.
https://jminie.tistory.com/180
오늘은 Hilt를 프로젝트에 적용해보자
📌 의존성주입
build.gradle(Project)
plugins {
...
...
id 'com.google.dagger.hilt.android' version '2.41' apply false
}
- 2022년 11월 버전은 2.41이다
- 버전은 언제든 올라갈 수 있다.
build.gradle(Module)
plugins {
...
id 'dagger.hilt.android.plugin'
}
android {
...
...
kapt {
correctErrorTypes true
}
}
dependencies {
...
...
// Hilt
implementation 'com.google.dagger:hilt-android:2.41'
kapt 'com.google.dagger:hilt-compiler:2.41'
}
- 이때 kapt가 에러 타입을 알아서 판단할 수 있도록 kapt의 correctErrorTypes true 설정을 넣어주도록 하자
📌 Application 클래스 작성
Hilt 라이브러리는 Application와 ApplicationContext에 접근하기 위해 ApplicationClass가 필요하다.
@HiltAndroidApp
class BookSearchApplication : Application() {
...
}
- 보통 Application 클래스의 이름은 앱 이름 뒤에 Application을 붙여주는 식으로 많이 명명한다.
- @HiltAndroidApp 어노테이션에 의해 Singleton Component를 생성하게 된다.
- 따라서 앱이 살아있는 동안 의존성(Dependency)을 제공하는 역할을 하는 애플리케이션(Application) 레벨의 Component가 되는 것이다.
또한 Application 클래스를 생성한 뒤에는 AndroidManifest 파일에 application 태그에 name 설정을 꼭 해주어야 오류가 생기지 않는다.
<application
android:name=".BookSearchApplication"
...
...
</application>
이 상태에서 프로젝트를 빌드하면 안드로이드 스튜디오 좌측 project 탭의 java(generated)에 Hilt에 의해서 자동으로 의존성 그래프(Dependency Graph) 파일이 생성되는 것을 확인할 수 있다.
📌 AppModule 클래스 생성
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
...
}
- AppModule은 앱 전체에서 사용할 모듈이다. 따라서 SingletonComponent로 설정해주었다.
- 또한 Module임을 알리는 @Module 어노테이션을 붙여준다.
그다음 Retrofit 설정을 해보자
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
// Retrofit
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
val httpLoggingInterceptor = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.baseUrl(BASE_URL)
.build()
}
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): BookSearchApi {
return retrofit.create(BookSearchApi::class.java)
}
}
- Interceptor로써 로깅에 사용될 OkHttpClient를 provideOkHttpClient라는 함수로 만들고
- 그리고 Retrofit 객체를 생성하는 provideRetrofit 함수를 만들어주고 그 안에 매개변수에 OkHttpClient를 넣어준다.
- 마지막으로 Api Service 객체를 생성하는 provideApiService 함수를 만들어주면 된다.
안드로이드 스튜디오에서는 위 사진처럼 코드 좌측에 의존성을 GUI 형태로 알려주는 어시스턴트 기능을 제공하는데 저 GUI 버튼들을 눌러보면 실제 객체들의 의존성 관계를 쉽게 알 수 있다.
📌 RepositoryModule 클래스 생성
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
}
- RepositoryModule 역시 앱 전체에서 사용할 모듈이기 때문에 SingletonComponent로 설정해주었다.
- 또한 AppModule 클래스와 마찬가지로 Module임을 알리는 @Module 어노테이션을 붙여준다.
이제 RepositoryModule 내부 코드를 구현해보자
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Singleton
@Binds
abstract fun bindBookSearchRepository(
bookSearchRepositoryImpl: BookSearchRepositoryImpl,
): BookSearchRepository
@Singleton
@Binds
abstract fun bindBookReportRepository(
bookReportRepositoryImpl: BookReportRepositoryImpl,
): BookReportRepository
@Singleton
@Binds
abstract fun bindAlarmRepository(
alarmRepositoryImpl: AlarmRepositoryImpl,
): AlarmRepository
}
- 인터페이스인 BookSearchRepository, BookReportRepository, AlarmRepository를 @Binds 어노테이션을 통해 Hilt가 의존성 객체를 생성할 수 있도록 하였다.
- 세 함수 모두 인터페이스인 Repositroy를 반환하고 그 매개변수에는 구현체인 RepositoryImpl을 넣어준다.
- 주의할 점은 모듈(Module)과 함수가 모두 abstract 측 추상 클래스와 추상 함수여야 한다.
📌 의존성 주입하기
안드로이드 앱 아케텍쳐(AAC) 구조에 따르면 의존성 주입이 단방향으로 이루어진다.
- AppModule의 의존성을 Repository에 주입
- Repository 객체는 ViewModel에 주입
- 각 View에 ViewModel 객체를 주입
AppModule에서 만든 Retrofit 의존성을 Repository에 주입
@Singleton
class BookSearchRepositoryImpl @Inject constructor(
private val api: BookSearchApi
) : BookSearchRepository {
...
...
}
- RepositoryImpl에 @Singleton을 붙여서 의존성 주입 가능한 스코프로 지정해준다.
- 다음에는 @Inject constructor ( ... ) 를 붙여준다.
- 그러면 constructor 안에 있는 객체들은 Hilt가 주입하게 된다.
- Hilt를 사용하기 전에는 Object 키워드를 통해 전역 싱글톤으로 Retrofit을 구현했었는데 이로써 Hilt로 대체할 수 있다.
이제 주입받은 Api를 이용해 원하는 로직을 구현하면 된다.
@Singleton
class BookSearchRepositoryImpl @Inject constructor(
private val api: BookSearchApi
) : BookSearchRepository {
override suspend fun searchBooks(
query: String,
sort: String,
page: Int,
size: Int
): Response<SearchResponse> {
return api.searchBooks(query, sort, page, size)
}
}
Repository 의존성을 ViewModel에 주입
@HiltViewModel
class SearchViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
}
- 로직은 Retrofit 의존성을 Repository에 주입해줄 때와 동일하다
- 다만 이번에는 주입받을 대상이 ViewModel이기 때문에 @HiltViewModel을 어노테이션 붙여서 의존성 주입 가능한 스코프로 지정해준다.
- 다음에는 @Inject constructor ( ... ) 를 붙여준다.
이제 주입받은 Repository를 이용해 원하는 로직을 구현해보자
@HiltViewModel
class SearchViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
private val _searchWord = MutableSharedFlow<String>()
val searchWord = _searchWord.debounce { 200 }
private val _searchPagingResult = MutableStateFlow<PagingData<Book>>(PagingData.empty())
val searchPagingResult: StateFlow<PagingData<Book>> = _searchPagingResult.asStateFlow()
fun handleSearchWord(word: String) {
viewModelScope.launch(exceptionHandler) {
_searchWord.emit(word)
}
}
fun searchBooksPaging(query: String) {
viewModelScope.launch(exceptionHandler) {
bookSearchRepository.searchBooksPaging(query, getSortMode())
.cachedIn(viewModelScope)
.collect {
_searchPagingResult.value = it
}
}
}
}
ViewModel 의존성을 View에 주입
- 이제 View에 ViewModel 의존성을 주입할 것이다.
- 이때 Delegate 패턴으로 ViewModel을 초기화하기 위해서 ktx 의존성을 추가해주어야 한다.
dependencies {
...
...
// Hilt
implementation 'com.google.dagger:hilt-android:2.41'
kapt 'com.google.dagger:hilt-compiler:2.41'
// ViewModel delegate
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
}
- Delegate 패턴을 사용하면 ViewModelFactory를 사용하지 않고도 ViewModel을 생성할 수 있다.
📌 1:1 vs N:1
이제 두 가지의 예제를 보자
- 첫 번째 예제는 하나의 View(Fragment)에 하나의 ViewModel을 가지고 있는 1:1 구조를 바탕으로 작성하였다.
- 두 번째 예제는 여러 개의 View(Fragment)에 하나의 ViewModel을 두고 있는 N:1 구조를 바탕으로 작성하였다.
- 여러 개의 View, 즉 Activity에 ViewModel을 두어 그 Activity에 종속되어 있는 여러 Fragment에서 하나의 ViewModel을 공유하는 N:1 구조는 하나의 ViewModel에서 데이터의 공유가 가능하다는 장점이 있지만 관심사의 분리 측면에서는 바람직하지 않을 수 있다.
📌 1:1
FragmentA
@AndroidEntryPoint
class SearchFragment : Fragment() {
...
...
private val searchViewModel: SearchViewModel by viewModels()
}
FragmentB
@AndroidEntryPoint
class BookReportFragment : Fragment() {
...
...
private val bookReportViewModel: BookReportViewModel by viewModels()
}
- View에 @AndroidEntryPoint을 붙여서 View를 의존성 주입 가능한 스코프로 지정해준다.
- ViewModel을 분리하면 이렇게 View에서 필요한 로직을 나누어 ViewModel에 저장하게 되므로 하나의 ViewModel이 비대해지는 것을 방지할 수 있다.
- by viewModels() 키워드를 사용하여 ViewModelFactory를 사용하지 않고 ViewModel을 초기화하였다.
- by viewModels()는 Activity와 Fragment 모두 사용 가능하며, by viewModels()를 이용한 초기화는 해당 ViewModel이 초기화되는 Activity 혹은 Fragment의 Lifecycle에 종속된다.
📌 N:1
하지만 하나의 ViewModel에서, 즉 Fragment를 가지고 있는 Activity의 ViewModel에서 가지고 있는 데이터를 여러 개의 Fragment에서 공유하고 싶을 수 있다.
Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
...
private val mainViewModel: MainViewModel by viewModels()
}
FragmentA
@AndroidEntryPoint
class FragmentA : Fragment() {
...
...
private val mainViewModel: MainViewModel by activityViewModels()
}
FragmentB
@AndroidEntryPoint
class FragmentB : Fragment() {
...
...
private val mainViewModel: MainViewModel by activityViewModels()
}
- 우선 전제는 FragmentA와 FragmentB가 Activity에 종속되어 있는 상황이다.
- by activityViewModels()는 Fragment에서만 사용 가능한 viewModel 초기화 방식이다.
inline fun <reified VM : ViewModel> Fragment.activityViewModels
내부 코드를 보아도 Fragment에 대한 확장 함수로 짜여 있는 것을 확인할 수 있다.
- Fragment는 Activity에 종속되어 있기 때문에 Fragment가 생성된 Activity의 Lifecycle에 ViewModel이 종속되어 있다.
'Android' 카테고리의 다른 글
안드로이드 [Kotlin] - 안드로이드에서의 싱글톤패턴(Singleton Pattern) with object & DCL (0) | 2022.12.09 |
---|---|
안드로이드 [Kotlin] - 안드로이드 테스트 자동화 (0) | 2022.11.27 |
안드로이드 [Kotlin] - 의존성 주입(DI) 알아보기 - Hilt (0) | 2022.11.07 |
안드로이드 [Kotlin] - WorkManager의 탄생배경과 활용 (0) | 2022.10.24 |
안드로이드 [Kotlin] - ViewModel 파헤쳐보기 (0) | 2022.10.03 |