https://github.com/kimmandoo/android-drill/tree/main/TmapWithNaverMap
필요한 기능 학습할 때 API를 써야되는 경우가 있다. 이때 retrofit을 사용해도 되지만 내 귀찮음이 Ktor로 이끌었다. 물론 제대로 하려면 Ktor도 모듈 분리하고 다 해야될 것이지만 간단하게 테스트 용도로 사용할 것이므로 큰 문제 없다고 생각한다.
이번에 해볼 건 TMAP 대중교통 API 불러오기다.
https://openapi.sk.com/products/detail?svcSeq=59&menuSeq=492
# Ktor
Ktor는 이름에서도 알수 있듯이 코틀린으로만 작성된 프레임워크다. 그리고 retrofit에 비해 좀 더 가볍다고 하는데 아직 제대로 안써봐서 잘 모르겠다.
gradle dependency는 아래와 같이 추가해주면 된다.
[versions]
ktorClientCore = "2.3.12"
[libraries]
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCore" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
// 여기까지는 toml에 작성
// app 수준 build.gradle.kts에 이걸 추가하면된다.
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
이러면 Ktor를 사용할 준비가 끝이 났다. 근데 안드로이드 특화 엔진을 사용하기 위해서 하나를 더 추가해보자.
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktorClientCore" }
implementation(libs.ktor.client.android)
이제 가장 간단한 코드를 작성해보겠다. 예시는 그냥 ktor 홈페이지로 해봤다.
private fun ktorBasic(){
val client = HttpClient(Android)
lifecycleScope.launch {
val response: String = client.get("https://ktor.io/").bodyAsText()
Log.d(TAG, "ktorBasic: $response")
}
}
비동기 웹 프레임워크라서 코루틴안에 넣어줘야된다.
이제 진짜 API를 호출해보자.
Json으로 내려올 거니까 이걸 처리해줄 dependency도 추가해준다. 직렬화에 사용되는 `kotlinx-serialization-json`도 같이 넣어줘야된다. ktor가 내부적으로 사용하기 때문이다. 그리고 자동 직렬화를 도와주는 ContentNegotiation도 추가해주겠다. 레트로핏으로 생각하면 일종의 converter factory라고 생각하면 될 것 같다.
결과적으로 아래와 같이 됐다.
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.android)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)
이제 진짜 Tmap API를 불러보자. 근데 아직 테스트 단계라서 데이터클래스 만들어서 자동 파싱까지 하는 건 건너뛰겠다.
api키 숨기는 건 다른 블로그를 찾아보거나 아래 링크를 읽어보면된다.
클라이언트 먼저 만들면 아래와 같다.
val client = HttpClient(Android) {
install(ContentNegotiation) {
json()
headers {
append("Accept", "application/json")
append("Content-Type", "application/json")
}
}
}
헤더를 붙여준다. 이때 client는 범용이라 appKey는 api 호출 부에서 넣어준다. BuildConfig가 안나온다면 프로젝트를 rebuild해보자. content-type은 기본헤더로 설정해도 될 것 같은데 아직 방법을 몰라서 넘어갔다.
바디를 이렇게 넘겨줘야한다. 바디 정도는 나중에도 쓸거니까 데이터클래스를 만들어줬다.
@Serializable
data class TmapRouteRequest(
val count: Int = 1,
val endX: String,
val endY: String,
val format: String = "json",
val lang: Int = 0,
val startX: String,
val startY: String
)
이제 client에 post요청을 하면서 setBody에 이 객체를 넣어주면 끝난다. 아까 직렬화, 컨버터 다 넣어놨기 때문에 우리는 그냥 넣어주기만 하면 된다.
private fun requestTmapAPI() {
val client = HttpClient(Android) {
install(ContentNegotiation) {
json()
}
}
lifecycleScope.launch {
val response = client.post("https://apis.openapi.sk.com/transit/routes") {
headers {
append("Content-Type", "application/json")
append("appKey", BuildConfig.TMAP)
}
setBody(
TmapRouteRequest(
endX = "127.030406594109",
endY = "37.609094989686",
startX = "127.02550910860451",
startY = "37.63788539420793"
)
)
}
Log.d(TAG, "ktorPostAPI: ${response.body<TmapRouteResponse>()}")
}
}
위에서는 Json 만들기 귀찮다고 안만들었었는데 그냥 플러그인 써서 만들었다... 예시로 주어진 API 응답에서 빠진 게 좀 있어 직접 api를 호출해서 파라미터를 생성했다.
잘 나오는 걸 볼 수 있다. 이제 이 정보를 갖고 네이버 지도 위에 오버레이를 그리면 된다...
혹시 response data class 가 필요한 사람도 있을 테니 추가로 올려두겠다. 한 곳만 호출해본거라서 빠진 파라미터가 있을 수 있을지도 모르겠다.
package com.kimmandoo.tmapwithnavermap.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TmapRouteResponse(
@SerialName("metaData")
val metaData: MetaData
) {
@Serializable
data class MetaData(
@SerialName("plan")
val plan: Plan,
@SerialName("requestParameters")
val requestParameters: RequestParameters
) {
@Serializable
data class Plan(
@SerialName("itineraries")
val itineraries: List<Itinerary>
) {
@Serializable
data class Itinerary(
@SerialName("fare")
val fare: Fare,
@SerialName("legs")
val legs: List<Leg>,
@SerialName("pathType")
val pathType: Int,
@SerialName("totalDistance")
val totalDistance: Int,
@SerialName("totalTime")
val totalTime: Int,
@SerialName("totalWalkDistance")
val totalWalkDistance: Int,
@SerialName("totalWalkTime")
val totalWalkTime: Int,
@SerialName("transferCount")
val transferCount: Int
) {
@Serializable
data class Fare(
@SerialName("regular")
val regular: Regular
) {
@Serializable
data class Regular(
@SerialName("currency")
val currency: Currency,
@SerialName("totalFare")
val totalFare: Int
) {
@Serializable
data class Currency(
@SerialName("currency")
val currency: String,
@SerialName("currencyCode")
val currencyCode: String,
@SerialName("symbol")
val symbol: String
)
}
}
@Serializable
data class Leg(
@SerialName("distance")
val distance: Int,
@SerialName("end")
val end: End,
@SerialName("Lane")
val lane: List<Lane>? = null,
@SerialName("mode")
val mode: String,
@SerialName("passShape")
val passShape: PassShape? = null,
@SerialName("passStopList")
val passStopList: PassStopList? = null,
@SerialName("route")
val route: String? = null,
@SerialName("routeColor")
val routeColor: String? = null,
@SerialName("routeId")
val routeId: String? = null,
@SerialName("sectionTime")
val sectionTime: Int,
@SerialName("service")
val service: Int? = null,
@SerialName("start")
val start: Start,
@SerialName("steps")
val steps: List<Step>? = null,
@SerialName("type")
val type: Int? = null,
) {
@Serializable
data class End(
@SerialName("lat")
val lat: Double,
@SerialName("lon")
val lon: Double,
@SerialName("name")
val name: String
)
@Serializable
data class Lane(
@SerialName("route")
val route: String,
@SerialName("routeColor")
val routeColor: String,
@SerialName("routeId")
val routeId: String,
@SerialName("service")
val service: Int,
@SerialName("type")
val type: Int
)
@Serializable
data class PassShape(
@SerialName("linestring")
val linestring: String
)
@Serializable
data class PassStopList(
@SerialName("stationList")
val stationList: List<Station>
) {
@Serializable
data class Station(
@SerialName("index")
val index: Int,
@SerialName("lat")
val lat: String,
@SerialName("lon")
val lon: String,
@SerialName("stationID")
val stationID: String,
@SerialName("stationName")
val stationName: String
)
}
@Serializable
data class Start(
@SerialName("lat")
val lat: Double,
@SerialName("lon")
val lon: Double,
@SerialName("name")
val name: String
)
@Serializable
data class Step(
@SerialName("description")
val description: String,
@SerialName("distance")
val distance: Int,
@SerialName("linestring")
val linestring: String,
@SerialName("streetName")
val streetName: String
)
}
}
}
@Serializable
data class RequestParameters(
@SerialName("airplaneCount")
val airplaneCount: Int,
@SerialName("busCount")
val busCount: Int,
@SerialName("endX")
val endX: String,
@SerialName("endY")
val endY: String,
@SerialName("expressbusCount")
val expressbusCount: Int,
@SerialName("ferryCount")
val ferryCount: Int,
@SerialName("locale")
val locale: String,
@SerialName("reqDttm")
val reqDttm: String,
@SerialName("startX")
val startX: String,
@SerialName("startY")
val startY: String,
@SerialName("subwayBusCount")
val subwayBusCount: Int,
@SerialName("subwayCount")
val subwayCount: Int,
@SerialName("trainCount")
val trainCount: Int,
@SerialName("wideareaRouteCount")
val wideareaRouteCount: Int
)
}
}
도움이 됐다면 댓글이나 공감 버튼 한 번씩 누르고 가주세요!
'Android 🖥️ > 삽질⛏️' 카테고리의 다른 글
Expandable FAB(Floating Action Button) 구현하기 - ListPopupWindow (0) | 2024.07.20 |
---|---|
Tmap 대중교통 API + Navermap PathOverlay로 경로선 그리기 (0) | 2024.07.14 |
Widget 알아보기(1) - 위젯에서 위치 정보 받기 (0) | 2024.07.09 |
Kotlin 한글 종성 구분하기 - 체언에 따라 조사 다르게 붙이기 (0) | 2024.05.24 |
서버없이 FCM 보내기 - CloudFunction + FireStore + FCM(2) (0) | 2024.05.19 |