2022. 3. 21. 17:48ㆍKotlin/Kotlin Side Project
Hilt는 Dagger2 보다 학습곡선이 낮고, Android에서 DI를 위한 Boilerplate 코드를 annotation 적용만으로 없앤 라이브러리이다.(Dagger를 기반으로 Google에서 2020년 6월 안드로이드 전용으로 Hilt를 발표했다.)
별도의 컴포넌트를 생성하지 않고, Application 상속 클래스에 @HiltAndroidApp annotation을 통해 최상단 컴포넌트인 SigletonComponent(구 ApplicationComponent)를 생성해준다.
@HiltAndroidApp
class App : Application() {
}
기존에 App에서 Context를 제공하던 방식이나 Dagger를 통해 @Component.Factory에서 App을 넘겨 직접 Context에 대한 코드를 짰던 것과 달리 Hilt에서는 @ApplicationContext, @ActivityContext 등을 제공해 필요한 곳에 붙여주기만 하면 된다.
적용될 컴포넌트를 @InstallIn으로 명시해주어 기존 Dagger에서 사용하던 @Component(modules=[])를 대신해 연결해준다. SigletonComponent로 명시해주었으나, @Singleton을 붙여 스콥을 지정해주는 것이 권장된다.
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
@Singleton
fun providePrefHelper(@ApplicationContext context: Context) : PrefHelper {
return PrefHelperImpl(context)
}
}
Activity의 경우, @AndroidEntryPoint annotation만 추가하면 DI를 적용할 수 있다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var adapter: PagingAdapter
// ui 관련된 DI는 설정하지 않는 것이 권장됨(activity -> binding -> activity의 순환주입이 발생할 수 있기 때문)
private lateinit var binding: ActivityMainBinding
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
//val viewModel = ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java)
lifecycleScope.launch {
viewModel.getContent().collectLatest {
adapter.submitData(it)
}
}
}
}
여기서 ViewModel은 인자 여부 상관없이 android-ktx의 by viewModels()를 통해 생성할 수 있다.(2.31 이후 추가된 AssistedInject 기능을 통해 런타임에 객체를 제공할 수 있게 됨. 기존은 명시된 Graph에서 정의된 Module에서만 주입받을 수 있었음)
Viewmodel 또한 별다른 코드 없이 @HiltViewModel과 생성자 @Inject를 통해 DI를 통해 사용될 수 있다.
@HiltViewModel
class MainViewModel @Inject constructor(val getPopularMovies: GetPopularMovies) : ViewModel() {
// suspend fun getContentLiveData() : LiveData<PagingData<PopularMovie>> {
// Log.i("KMJTEST","[MainViewModel] getContentLiveData()")
// return getPopularMovies.buildUseCase()?.asLiveData()
// }
suspend fun getContent(): Flow<PagingData<PopularMovie>> = getPopularMovies.buildUseCase()
}
Dagger에서 ViewModel을 바인딩시켜 구성한 Map에서 Provider를 제공했던 ViewModelFactory가 필요 없어짐에 따라 ViewModelModule, ViewModelFactoryModule, ViewModelFactory 클래스는 제거한다.
기존에 필수 모듈이었던 AndroidSupportInjectionModule, ActivityBindingModule은 이제 더 이상 필요하지 않으며, 그밖의 Retrofit, Room 관련 모듈(SigletonComponent 연결), UseCase, Repository 관련 모듈(ViewModelComponent 연결)로 진행하면 된다.
SigletonComponent 연결
@Module
@InstallIn(SingletonComponent::class)
class RetrofitModule {
@Provides
@Singleton
fun provideGsonConvertFactory(): GsonConverterFactory {
val gson = GsonBuilder()
.setLenient()
.create()
return GsonConverterFactory.create(gson)
}
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
}
@Provides
@Singleton
fun provideRetrofit(
@ApplicationContext context: Context,
client: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
): Retrofit {
return Retrofit.Builder()
.baseUrl(context.getString(R.string.base_url))
//.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.addConverterFactory(gsonConverterFactory)
.build()
}
@Provides
@Singleton
fun provideTMDBService(retrofit : Retrofit) : TMDBService {
return retrofit.create(TMDBService::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
class RoomModule {
@Provides
@Singleton
fun provideLocalMovieDatabase(@ApplicationContext context : Context) : LocalMovieDatabase {
return Room.databaseBuilder(
context,
LocalMovieDatabase::class.java,
"popular_database"
).fallbackToDestructiveMigration()
.build()
}
@Provides
@Singleton
fun providePopularMovieDao(localMovieDatabase: LocalMovieDatabase) : PopularMovieDao {
return localMovieDatabase.popularMovieDao()
}
}
ViewModelComponent 연결
@Module
@InstallIn(ViewModelComponent::class)
class UseCaseModule {
@Provides
fun provideGetPopularMovies(repository: PopularMovieRepository): GetPopularMovies {
return GetPopularMovies(repository)
}
}
@Module
@InstallIn(ViewModelComponent::class)
class RepositoryModule {
@Provides
fun providePagingConfig(): PagingConfig =
PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 3)
@Provides
fun provideRefreshControl(prefHelper: PrefHelper): RefreshControl =
RefreshControl(TimeUnit.MINUTES.toMillis(5), prefHelper = prefHelper)
@OptIn(ExperimentalPagingApi::class)
@Provides
fun providePopularMoviesPager(
config: PagingConfig,
db: LocalMovieDatabase,
api: TMDBService,
refreshControl: RefreshControl,
popularMovieDao: PopularMovieDao
): Pager<Int, PopularMovieLocal> = Pager(config = config, remoteMediator = PopularMovieMediator(
db,
api,
refreshControl
), pagingSourceFactory = {
popularMovieDao.getPopularMoviesPagingSource()
})
@Provides
fun providePopularMovieRepository(
db: LocalMovieDatabase,
api: TMDBService,
pager : Pager<Int, PopularMovieLocal>
): PopularMovieRepository {
return PopularMovieRepositoryImpl(db, api, Dispatchers.IO, pager)
}
}
[WIP] UseCaseModule, RepostitoryModule의 Scope은 SingletonComponent일까 ViewModelComponent일까?
UseCase에는 Repository가 필요하다. Repository의 구현체에서는 Pager가 필요하다.
Pager는 데이터 스트림이기 때문에 새로운 요청 시 새롭게 제공될 필요가 있다.(뇌피셜)
그럼 Pager는 ViewModelComponent에서만 유일해야하므로 ViewModelComponent+@Singleton이다.
Repository도 하나의 ViewModel에서는 하나만 제공되면 되므로 @Singleton으로 지정하는데, 만약 SingletonComponent로 지정해버리면 단 한 번만 생성이 진행되기 때문에 Pager가 새롭게 제공되어도 사용되지 않는다. 따라서, ViewModelComponent+@Singleton으로 지정해야한다.
마찬가지로, Repository 구현체를 주입받는 UseCase도 ViewModel 내에서는 하나면 되는데, SingletonComponent로 지정 시 Repository 구현체가 새롭게 생성되어도 기존 UseCase 인스턴스를 재사용할 것이기 때문에 소용이 없다. 따라서 이 UseCase도 ViewModelComponent+@Singleton으로 지정되어야 한다.
'Kotlin > Kotlin Side Project' 카테고리의 다른 글
[Kotlin Side Project] Dagger2 적용 (1) | 2022.03.21 |
---|---|
[Kotlin Side Project] Paging3 적용(Offline-first) (0) | 2022.03.04 |
[Kotlin Side Project] Clean Architecture 구현 (0) | 2022.03.04 |
[Kotlin Side Project] 구현 목표 (1) | 2022.03.04 |
[Kotlin Side Project] RemoteMediator 무한 로딩 (0) | 2021.11.18 |