선수 지식에 대한 기본 바탕이 있어야 내용을 이해 가능합니다.
1. Hilt-Dependency-Injection란?
2. Backing Properties 란?이제부터는 DI와 ViewModel을 통한 MVVM 구조로 코드가 변경이 됩니다. 이전에는 하나의 라이브러리를 사용하는 관점이라면 이제는 아키텍처로 어떻게 구현해야 하는지의 관점이기 때문에 코드의 구성이 싹 바뀌게 됩니다. 코드가 바뀌는 과정은 커밋으로 볼 수 있습니다.
1. NetworkModule
//networkmodule 패키지 각각의 annotation 클래스
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class ShopRetrofitInstance
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class TmapRetrofitInstance
@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
interceptor.level =
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
}
@Provides
@Singleton
fun provideConverterFactory(): GsonConverterFactory {
return GsonConverterFactory.create()
}
@TmapRetrofitInstance
@Provides
@Singleton
fun provideTmapRetrofitInstance(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
): Retrofit {
return Retrofit.Builder()
.baseUrl(Url.TMAP_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
}
@ShopRetrofitInstance
@Provides
@Singleton
fun provideShopRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
): Retrofit {
return Retrofit.Builder()
.baseUrl(Url.SHOP_URL)
.addConverterFactory(gsonConverterFactory)
.client(okHttpClient)
.build()
}
@Provides
@Singleton
fun provideMapApiService(@TmapRetrofitInstance retrofit: Retrofit): MapApiService {
return retrofit.create(MapApiService::class.java)
}
@Provides
@Singleton
fun provideShopController(@ShopRetrofitInstance retrofit: Retrofit): ShopController {
return retrofit.create(ShopController::class.java)
}
}
annotation class를 통해서 반환형이 Retrofit으로 같은 provideTmap 과 provideShop RetrofitInstance를 구분해 줍니다. 그리고 각각 MapApiService 와 ShopController를 반환해서 서버의 endpoint와 통신 할 수있게 만들어 줍니다.
2. DispatcherModule
@Module
@InstallIn(ApplicationComponent::class)
object DispatcherModule {
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
//dispatchermodule 패키지 각각의 annotation 클래스
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainDispatcher
코루틴의 사용을 위해서 CoroutineDispatcher를 분류하여 디스패처들을 각각 어노테이션을 달아 주입을 위한 모듈을 만들어 줍니다.
3. MapApiRepositoryImpl
class MapApiRepositoryImpl @Inject constructor(
private val mapApiService: MapApiService,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : MapApiRepository {
override suspend fun getReverseGeoInformation(locationLatLngEntity: LocationEntity) =
withContext(ioDispatcher) {
val response = mapApiService.getReverseGeoCode(
lat = locationLatLngEntity.latitude,
lon = locationLatLngEntity.longitude
)
if (response.isSuccessful) {
response.body()?.addressInfo
} else {
null
}
}
}
MapApiRepositoryImpl에는 위에서 DI 작업이 다 끝난 MapApiService를 생성자 주입을 해줍니다. 그리고 코루틴을 써야하기 때문에 위 작업은 IO 작업이라서 @IoDispatcher 어노테이션은 선언을 하고 똑같이 생성자 주입을 해 줍니다.
4. ShopApiRepositoryImpl
class ShopApiRepositoryImpl @Inject constructor(
private val shopController: ShopController,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ShopApiRepository {
override suspend fun getShopList() =
withContext(ioDispatcher) {
val response = shopController.getList()
if (response.isSuccessful) {
response.body()
} else {
null
}
}
}
ShopApiRepositoryImpl 위에서 DI 작업이 다 끝난 ShopController를 생성자 주입을 해줍니다. 그리고 코루틴을 써야하기 때문에 위 작업은 IO 작업이라서 @IoDispatcher 어노테이션은 선언을 하고 똑같이 생성자 주입을 해 줍니다. 결론은 위의 방식과 동일합니다.
5. RepositoryBindModule
@Module
@InstallIn(ApplicationComponent::class)
abstract class RepositoryBindModule {
@Binds
abstract fun bindMapApiRepository(
mapApiRepositoryImpl: MapApiRepositoryImpl
): MapApiRepository
@Binds
abstract fun bindShopApiRepository(
shopApiRepositoryImpl: ShopApiRepositoryImpl
): ShopApiRepository
}
생성자 주입이 된 MapApiRepositoryImpl와 ShopApiRepositoryImpl를 인터페이스로 반환하여 쓸 수 있도록 바인드 해줍니다. 바인드를 해줘야 인터페이스로 반환하여 사용할 수 있게 됩니다.
6. 주입할 ViewModel들
class MainViewModel
@ViewModelInject
constructor(
private val mapApiRepositoryImpl: MapApiRepository
) : ViewModel() {}
class MapViewModel
@ViewModelInject
constructor(
private val ShopApiRepositoryImpl : ShopApiRepository
) : ViewModel() {}
위와 같이 ViewModel에 생성자 주입을 해서 사용하거나 클린아키텍쳐의 계층에 따라 사용시에는 UseCase에 주입해서 사용을 할 수 있습니다.
7. 사용할 컴포넌트에 필드 삽입
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
....
}
@AndroidEntryPoint
class MapFragment : Fragment() , OnMapReadyCallback {
private val viewModel: MapViewModel by viewModels()
....
}
컴포넌트에 필드 삽입을 하여 뷰모델을 통한 비즈니스 로직을 이용할 수 있게 됩니다.
1. RepositoryModule
//기존 DB 싱글턴으로 구현
@Database(entities = [AddressHistoryEntity::class], version = 1)
abstract class MapDB : RoomDatabase() {
companion object {
private var instance: MapDB? = null
fun getInstance(_context: Context): MapDB? {
if(instance == null) {
synchronized(MapDB::class) {
instance = Room.databaseBuilder(_context.applicationContext,
MapDB::class.java, "MapStudy.db")
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build()
}
}
return instance
}
}
abstract fun addressHistoryDao(): AddressHistoryDao
}
//hilt를 사용하여 구현
@Module
@InstallIn(ApplicationComponent::class)
object RepositoryModule {
@Provides
@Singleton
fun provideMapDB(@ApplicationContext context: Context): MapDB =
Room.databaseBuilder(context, MapDB::class.java, "MapStudy.db")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
기존에 임의로 코드로 만들었던 싱글턴을 이제는 hilt를 사용하여 싱글턴으로 DB를 만들어 줍니다.
2. AddressHistoryRepository
class AddressHistoryRepository @Inject constructor(
private val addressHistoryDB : MapDB,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
suspend fun getAllAddresses() = withContext(ioDispatcher) {
addressHistoryDB.addressHistoryDao().getAllAddresses()
}
suspend fun insertAddress(address: AddressHistoryEntity) = withContext(ioDispatcher) {
addressHistoryDB.addressHistoryDao().insertAddress(address)
}
suspend fun deleteAddress(address: AddressHistoryEntity) = withContext(ioDispatcher) {
addressHistoryDB.addressHistoryDao().deleteAddress(address)
}
suspend fun deleteAllAddresses() = withContext(ioDispatcher) {
addressHistoryDB.addressHistoryDao().deleteAllAddresses()
}
}
suspend modifier라서 withContext 로 내부DB의 CRUD를 처리해 줍니다.
1. MainViewModel
class MainViewModel
@ViewModelInject
constructor(
private val mapApiRepositoryImpl: MapApiRepository
) : ViewModel() {
....
private val _locationData = MutableLiveData<MainState>(MainState.Uninitialized)
val locationData: LiveData<MainState> = _locationData
....
}
Backing Properties 형태로 LiveData를 구현을해서 LiveData의 상태를 MainActivity에서 임의로 수정할 수 없게 만들어 줍니다. 이렇게 하면 MainActivity에서는 LiveData의 data 상태의 변화만 읽고 상태에 따라 View와 data를 업데이트 하게 됩니다. Backing Properties가 무엇인지 추가 적인 내용은 위의 선수 지식의 링크를 통해서 공부 할 수 있습니다.
2. MainActivity
class MainActivity : AppCompatActivity() {
....
private val viewModel: MainViewModel by viewModels()
@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
....
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
observeData()
....
}
private fun observeData() = with(binding) {
viewModel.locationData.observe(this@MainActivity) {
when (it) {
is MainState.Uninitialized -> {
if(viewModel.getMyLocation(this@MainActivity)) {
permissionLauncher.launch(PERMISSIONS)
}
}
is MainState.Loading -> {}
is MainState.Success -> {
locationLoading.isGone = true
locationTitleTextView.text = it.mapSearchInfoEntity.fullAddress
viewModel.setDestinationLocation(it.mapSearchInfoEntity.locationLatLng)
}
is MainState.Error -> {
locationTitleTextView.text = getString(it.errorMessage)
}
}
}
}
....
}
MainViewModel의 LiveData가 Backing Properties로 구현 되어있고, MainActivity에서는 Observing을 통해서 LiveData의 변화를 sealed Class의 state에 따라서 구분해서 UI 갱신 및 비즈니스 로직을 실행하는 것을 볼 수 있습니다.