전체 코드 길이가 길고 다뤄야 하는 클래스도 많기 때문에 설명에 필요한 일부 코드만 다뤄보겠습니다.
1. MainActivity.kt (fun onCreate())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
var mapFragment: MapFragment =
supportFragmentManager.findFragmentById(R.id.map_fragment) as MapFragment
mapFragment.getMapAsync(this)
locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
uiScope = CoroutineScope(Dispatchers.Main)
getApiShopList()
binding.btnSearchAround.setOnClickListener {
updateMarker()
}
}
이전글과 다르게 추가된 기능이 3개가 있습니다. 비동기로 통신을 위한 uiScope와 실제로 데이터를 가져올 fun인 getApiShopList() 그리고 데이터를 가져오고 그 데이터를 표시하기 위한 updateMarker() 입니다. 차근히 알아보록 하겠습니다.
1. Endpoint를 생성
위와 같이 Endpoint interface를 만들어서 서버에 접근해서 통신할 수 있게 해줍니다. Endpoint에서 URL에 대한 결과값을 반환 받을 메서드는 GET으로 하였습니다.
2. Retrofit과 OkHttpClient
object RetrofitUtil {
private var shopinstance: Retrofit? = null
val shopController: ShopController by lazy {
provideShopRetrofit().create(ShopController::class.java)
}
private fun provideShopRetrofit(): Retrofit {
if(shopinstance == null) {
shopinstance = Retrofit.Builder()
.baseUrl(Url.SHOP_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(buildOkHttpClient())
.build()
}
return shopinstance!!
}
private fun buildOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
interceptor.level =
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
}
}
RetrofitUtil은 기존의 객체를 재활용 하기 위해서 싱글턴으로 구현하였습니다. 그리고 Response와 Request에 대한 정보를 보기 위해서 OkHttpClient를 사용하여 val interceptor = HttpLoggingInterceptor() 객체를 생성해서 Retrofit에 추가해 줍니다.
3. MainActivity.kt (fun getApiShopList())
fun getApiShopList() {
uiScope.launch {
withContext(Dispatchers.IO) {
val response = RetrofitUtil.shopController.getList()
if (response.isSuccessful) {
val list = response.body()
list?.let {
it.shopList.forEach { ShopData ->
shopList.add(ShopData)
}
}
}
else {
null
}
}
}
}
레트로핏을 사용하여 http 통신을 진행합니다. 그 과정에서 비동기 처리를 하기 위해서 코루틴을 사용하게 됩니다. Dispatchers.IO로 하여서 IO 전용의 쓰레드를 사용하여 데이터를 가져오는 것을 확인 할 수 있습니다. 안드로이드 경우 UI 쓰레드를 사용하면 ANR로 앱이 멈추는 현상이 나타날 수 있기 때문에 UI 쓰레드에서는 비용이 큰 작업을 하면 안됩니다.
private var shopList: MutableList<ShopData> = mutableListOf()
이렇게 받아온 data를 위의 배열에 넣어줍니다.
1. MainActivity.kt (fun updateMarker())
private fun updateMarker() {
deleteMarkers()
var markets: List<ShopData> = mutableListOf()
var temp = arrayListOf<Marker>()
var i = 0
markets = shopList
markets?.let {
repeat(markets.size) {
temp += Marker().apply {
position = LatLng(markets[i].latitude, markets[i].longitude)
icon = MarkerIcons.BLACK
tag = markets[i].shop_name
zIndex = i
}
i++
}
markers = temp
searchAround()
}
}
위에서 받아온 전역으로 저장된 shopList가 있습니다. 그것을 markets = shopList으로 넣어주고 markets의 갯수만큼 repeat하여 반복문을 시행해줍니다. 그리고, temp에 markets에 들어있는 정보중 필요한 정보를 마커에 넣어서 넣어준뒤 마커의 배열에 또 저장을 해주게 되면 마커는 shopList에서 받아온 데이터를 가지게 됩니다.
private fun searchAround() {
deleteMarkers()
for (marker in markers) {
marker.map = naverMap
setMarkerIconAndColor(marker, getCategoryNum(shopList.get(marker.zIndex)!!.category))
}
}
private fun deleteMarkers() {
for (marker in markers) {
marker.map = null
}
}
기존의 배열에 있는 마커를 지우고 난 뒤에 다시 마커를 새롭게 출력해 줍니다.
1. MainActivity.kt (fun calDist())
fun calDist(lat1:Double, lon1:Double, lat2:Double, lon2:Double) : Long {
val EARTH_R = 6371000.0
val rad = Math.PI / 180
val radLat1 = rad * lat1
val radLat2 = rad * lat2
val radDist = rad * (lon1 - lon2)
var distance = Math.sin(radLat1) * Math.sin(radLat2)
distance = distance + Math.cos(radLat1) * Math.cos(radLat2) * Math.cos(radDist)
val ret = EARTH_R * Math.acos(distance)
return Math.round(ret)
}
두 점사이의 직선 거리를 구하는 함수 입니다. 이를 통해서 원안에 상점이 들어와 있는지 체크할 수 있습니다.
2. MainActivity.kt (fun updateMarker())
private fun updateMarker() {
deleteMarkers()
var markets: List<ShopData> = mutableListOf()
var temp = arrayListOf<Marker>()
var i = 0
markets = shopList
markets?.let {
repeat(markets.size) {
val dist = calDist(
curLocation.latitude,
curLocation.longitude,
markets[i].latitude,
markets[i].longitude)
if (dist < DISTANCE ) {
temp += Marker().apply {
position = LatLng(markets[i].latitude, markets[i].longitude)
icon = MarkerIcons.BLACK
tag = markets[i].shop_name
zIndex = i
}
}
i++
}
markers = temp
searchAround()
}
}
마커를 업데이트 할 때 DISTANCE 조건에 맞게 거리에 따라서 마커를 temp에 넣어줍니다. 그리고 난 뒤 출력을 해주게 되면 아래와 같은 그림의 원 범위로 존재하는 상점만 출력되는 것을 볼 수 있습니다.
private fun setMarkerListener() {
for (marker in markers) {
var tempinfoWindow = InfoWindow()
tempinfoWindow?.adapter = object : InfoWindow.DefaultTextAdapter(this) {
override fun getText(infoWindow: InfoWindow): CharSequence {
return infoWindow.marker?.tag as CharSequence
}
}
infoWindow = tempinfoWindow
marker.setOnClickListener {
if(tempinfoWindow?.marker != null) {
tempinfoWindow?.close()
} else {
tempinfoWindow?.open(marker)
}
true
}
}
}
var tempinfoWindow = InfoWindow()에서 InfoWindow 객체를 각각 생성 한 뒤 tempinfoWindow?.marker로 InfoWindow를 각각 객체마다 껏다가 켯다가 할 수 있습니다.