이때까지는 맵 구현의 개별적인 개념에 대해서 알아보았습니다. 이제는 이러한 맵 구현 개별적인 개념이 다른 개념들과 어떻게 적용이 되었는지 알아보도록 하겠습니다.
이해를 위한 선수 지식 목록
프로젝트의 내부 구조에 따라 구현 방법은 천차 만별이 됩니다. 예를 들어 대표적으로는 MVC , MVP , MVVM 이 있을 것 입니다. 구현 화면을 기준으로 각각의 화면을 구현할 때 종속성을 어떻게 나눠 구현했는지에 대해 설명하겠습니다.
위드 마켓이라고 된 로고의 상단을 보면, 주소가 있는 것을 확인할 수 있습니다. 그리고 아래에는 bottom navigation이 있습니다. 주소를 클릭시 옆의 주소설정 Activity가 열리게 됩니다.
위드 마켓은 위치값을 MainActivity에서 처음 받아오게 됩니다. 그리고 그 위치값을 변경하는 것은 주소설정 Activity에서 진행하게 되고 이용하는 것은 bottom navigation 중 지도 fragment만 이용하고 있습니다. 그래서 이전에 제가 개념 설명을 위해서 진행한 지도 프로젝트와는 상황이 다르게 됩니다.
위의 NaverMap앱은 MainActivity에서 모든 것을 진행하기 때문에 위치를 받아오면 즉시 액티비티 내에서 다 지도 관련 처리를 할 수가 있는데, 위드 마켓의 경우는 fragment로 위치를 전송 시켜서 똑같은 내용을 처리해야 합니다. 거기에 MVVM 구조까지 생각하면서 비즈니스 로직을 분리해야 하는 상황입니다. 더 나아가 클린아키텍처를 생각하면서 Usecase별로 분리 구현할 수 있지만, 아직 거기까지는 진행하지 않았습니다. 추후에 진행할 예정입니다.
현재 앱의 지도 프레그먼트와 전체적인 구조를 동일하게 하려면 주소설정 Activity도 추가할 필요가 있습니다. 주소 설정을하고 내용은 Room 라이브러리를 통해 내부 DB에 저장되게 됩니다. 이 과정부터 시작해서 bottom navigation과 viewmodel을 통한 비즈니스 로직 분리 등을 차근히 알아보겠습니다.
// MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.locationTitleTextView.setOnClickListener {
try {
myLocationStartForResult.launch(
MyLocationActivity.newIntent(this, mapSearchInfoEntity)
)
} catch (ex: Exception) {
Toast.makeText(this, "myLocation 초기화 중", Toast.LENGTH_SHORT).show()
}
}
....
}
// MyLocationActivity.kt
class MyLocationActivity : AppCompatActivity() {
....
companion object {
const val MY_LOCATION_KEY = "MY_LOCATION_KEY"
fun newIntent(context: Context, mapSearchInfoEntity: MapSearchInfoEntity) =
Intent(context, MyLocationActivity::class.java).apply {
putExtra(MY_LOCATION_KEY, mapSearchInfoEntity)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
binding.btnSetLocation.setOnClickListener {
startForResult.launch(MapLocationSettingActivity.newIntent
(this, intent.getParcelableExtra(MY_LOCATION_KEY)!!)
)
}
}
....
}
원래의 위드 마켓처럼 위의 빨간색 박스의 주소를 누르면 주소설정 MyLocationActivity가 실행됩니다. Activity가 실행 될 때 mapSearchInfoEntity 객체를 전달하면서 Activity Instance가 생성되게 됩니다.
MainActivity에서 받은 mapSearchInfoEntity 데이터를 MyLocationActivity에서 이제 getParcelableExtra를 통해 받을 수 있습니다. 이를 활용하여 MyLocationActivity에서 MapLocationSettingActivity Instance를 생성할 때 MainActivity의 mapSearchInfoEntity를 넘겨주게 되어서 내 위치값을 받아서 Activity가 생성되게 됩니다.
위의 그림은 TMAP의 지도 위치 설정 API의 주소 data 흐름이 됩니다. WebView를 통한 카카오 주소검색 API의 경우도 동일한 경우라고 보면 됩니다. 최종적으로는 MainActivity의 빨간색 박스 주소를 변경 시켜줄 수 있습니다. 그리고 최근 주소는 Room을 사용해서 일부 갯수까지는 저장되서 어떠한 것을 검색했는지 확인 할 수 있습니다.
Room에는 다음 3가지 주요 구성요소가 있습니다.
- 데이터베이스 클래스 : 데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.
- 데이터 항목 : 앱 데이터베이스의 테이블을 나타냅니다.
- 데이터 액세스 객체(DAO) : 앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공합니다.
1. 데이터베이스 클래스
@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
}
싱글턴으로 데이터베이스가 동일 객체를 반환하게 만들었습니다. 데이터베이스가 만들어지면 abstract fun addressHistoryDao(): AddressHistoryDao 를 통해 데이터 베이스의 메서드를 활용해서 CRUD를 할 수 있게 됩니다.
2. 데이터 항목
@Entity(tableName = "MapStudy")
data class AddressHistoryEntity(
@PrimaryKey(autoGenerate = true) //키가 1,2,3,4 ... 순서대로 자동으로 할당
val id: Long?,
val name: String,
val lat: Double,
val lng: Double
)
데이터 베이스에 들어가는 데이터들의 목록이라고 볼 수 있습니다.
3. 데이터 액세스 객체(DAO)
@Dao
interface AddressHistoryDao {
@Query("SELECT * FROM MapStudy")
suspend fun getAllAddresses(): List<AddressHistoryEntity>
@Insert
suspend fun insertAddress(address: AddressHistoryEntity)
@Delete
suspend fun deleteAddress(address: AddressHistoryEntity)
@Query("delete from MapStudy")
suspend fun deleteAllAddresses()
}
데이터 베이스에서 활용할 메서드들 입니다. 위 메서드들로 우리가 원하는 데이터를 저장 및 수정 제거를 할 수 있습니다.
지도에서 위치 설정 버튼을 클릭하게 되면 왼쪽의 MapLocationSettingActivity가 활성화 됩니다. 처음의 지정된 주소에 새롭게 위치를 변경하여 주소를 바꿔 주게 되면 내부DB에 그 주소가 저장되게 됩니다. 그 과정을 코드상으로 어떻게 이루어지는지 아래에서 확인해보겠습니다.
class MyLocationActivity : AppCompatActivity() {
....
private lateinit var database: MapDB
private lateinit var uiScope: CoroutineScope
private val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{ result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val bundle = result.data?.extras
val result = bundle?.get("result")
saveRecentSearchItems(result as MapSearchInfoEntity)
Toast.makeText(this, result.toString(), Toast.LENGTH_LONG).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
....
database = MapDB.getInstance(this)!!
uiScope = CoroutineScope(Dispatchers.Main)
....
}
private fun saveRecentSearchItems(entity: MapSearchInfoEntity) = uiScope.launch {
val data = AddressHistoryEntity(
id = null,
name = entity.fullAddress,
lat = entity.locationLatLng.latitude,
lng = entity.locationLatLng.longitude
)
database.addressHistoryDao().insertAddress(data)
}
....
}
registerForActivityResult로 지도에서 위치 설정 버튼을 누르면 활성화 되는 MapLocationSettingActivity로부터 result값을 반환 받습니다. 그리고 saveRecentSearchItems(result as MapSearchInfoEntity)에 result 값을 넘겨서 database.addressHistoryDao().insertAddress(data)를 통해 Room으로 만들어둔 내부 DB에 값을 저장해 줍니다.
class MyLocationActivity : AppCompatActivity() {
private lateinit var database: MapDB
private lateinit var uiScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
....
database = MapDB.getInstance(this)!!
uiScope = CoroutineScope(Dispatchers.Main)
binding.btnClear.setOnClickListener {
uiScope.launch {
database.addressHistoryDao().deleteAllAddresses()
recentAddrAdapter.clear()
recentAddrAdapter.notifyDataSetChanged()
}
}
uiScope.launch {
//배열로 하나씩 받아서 넣어준다.
val addressHistory = database.addressHistoryDao().getAllAddresses()
for (allAddress in addressHistory) {
recentAddrAdapter.datas.add(allAddress)
}
binding.rvRecentAddr.adapter = recentAddrAdapter
}
recentAddrAdapter = RecentAddrAdapter { item ->
intent.putExtra(MY_LOCATION_KEY, MapSearchInfoEntity(item.name, item.name, LocationEntity(item.lat, item.lng)))
setResult(RESULT_OK, intent)
finish()
}
}
}
처음 MyLocationActivity가 활성화 되면 override fun onCreate(savedInstanceState: Bundle?)가 call되면서 uiScope.launch를 통해 싱글턴으로 객체가 생성된 Database에 접근하게 되서 database.addressHistoryDao().getAllAddresses() 로 내부 DB에 저장된 모든 주소 리스트를 가져옵니다. 그리고 binding.btnClear.setOnClickListener의 경우 이벤트 발생시 저장된 모든 주소가 삭제 됩니다.