[심화] NaverMap API 사용하기 (10)

쓰리원·2022년 7월 9일
0

안드로이드 지도API

목록 보기
12/12
post-thumbnail

선수 지식에 대한 기본 바탕이 있어야 내용을 이해 가능합니다.

1. Hilt-Dependency-Injection란?
2. Backing Properties 란?

이제부터는 DI와 ViewModel을 통한 MVVM 구조로 코드가 변경이 됩니다. 이전에는 하나의 라이브러리를 사용하는 관점이라면 이제는 아키텍처로 어떻게 구현해야 하는지의 관점이기 때문에 코드의 구성이 싹 바뀌게 됩니다. 코드가 바뀌는 과정은 커밋으로 볼 수 있습니다.


1. Dagger_Hilt를 @Bind로 의존성 주입

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()
    
    ....
}  

컴포넌트에 필드 삽입을 하여 뷰모델을 통한 비즈니스 로직을 이용할 수 있게 됩니다.

2. Dagger_Hilt를 @Provides로 의존성 주입

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를 처리해 줍니다.

3. AAC ViewModel을 사용해서 MVVM 구조로 LiveData를 활용한 뷰모델 옵저버 패턴 구현

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 갱신 및 비즈니스 로직을 실행하는 것을 볼 수 있습니다.

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글