Retrofit 실습 예제 (1) 시도별 미세먼지 현황 앱
2024. 7. 31. 21:44ㆍ[개발]/Kotlin 활용 앱 개발
1) 공공데이터 포털에서 API 인증 키 발급
https://www.data.go.kr/data/15073861/openapi.do
2) gradle에 라이브러리 추가(큰따옴표로 추가하기)
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
implementation("com.github.skydoves:powerspinner:1.2.6")
3) manifest 파일 코드 추가
// 인터넷 사용 권한
<uses-permission android:name="android.permission.INTERNET"/>
<application
...
// HTTP 타입에 대한 접근 허용
android:usesCleartextTraffic="true">
4) 코드 작성
// data class
package com.example.miseya
import com.google.gson.annotations.SerializedName
// 실제 받는 json 파일과 아주 똑같이 data class 를 생성
data class Dust(val response: DustResponse)
data class DustResponse(
// 원본 데이터의 이름을 카멜 표기법 형식으로 변환하기 위한 Serialize 작업
// body, header: json 파일에서 가장 먼저 들어오는 데이터(response 밑에 바디, 헤더)
@SerializedName("body")
val dustBody: DustBody,
@SerializedName("header")
val dustHeader: DustHeader
)
data class DustBody(
// body 안에 totalCount, dustItem, pageNo, numOfRows 데이터 존재
val totalCount: Int,
@SerializedName("items")
val dustItem: MutableList<DustItem>?,
val pageNo: Int,
val numOfRows: Int
)
data class DustHeader(
val resultCode: String,
val resultMsg: String
)
data class DustItem(
val so2Grade: String,
val coFlag: String?,
val khaiValue: String,
val so2Value: String,
val coValue: String,
val pm25Flag: String?,
val pm10Flag: String?,
val o3Grade: String,
val pm10Value: String,
val khaiGrade: String,
val pm25Value: String,
val sidoName: String,
val no2Flag: String?,
val no2Grade: String,
val o3Flag: String?,
val pm25Grade: String,
val so2Flag: String?,
val dataTime: String,
val coGrade: String,
val no2Value: String,
val stationName: String,
val pm10Grade: String,
val o3Value: String
)
package com.example.miseya
import retrofit2.http.GET
import retrofit2.http.QueryMap
interface NetWorkInterface {
@GET("getCtprvnRltmMesureDnsty") //시도별 실시간 측정정보 조회 주소
// HashMap 형태의 (키, 값)으로 Request Parameter들을 넣어 데이터를 요청 -> 리턴받는 데이터는 Dust(data class)
suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
}
package com.example.miseya
//import com.example.miseya.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NetWorkClient {
// 서비스 url값 입력
private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"
private fun createOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
// // 통신이 잘 안될 때 디버깅
// if (BuildConfig.DEBUG)
// interceptor.level = HttpLoggingInterceptor.Level.BODY
// else
// interceptor.level = HttpLoggingInterceptor.Level.NONE
// Timeout Interval 부여
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor)
.build()
}
// Retrofit은 항상 아래 형태로 정의하니까 기억해놓고 써먹기
private val dustRetrofit = Retrofit.Builder()
.baseUrl(DUST_BASE_URL)
// json을 gson으로 컨버팅: 어지러운 원본 json 파일을 깔끔한 data class 파일로 바꿔줌
.addConverterFactory(GsonConverterFactory.create())
.client(createOkHttpClient()
).build()
// NetworkInterface에서 Retrofit 생성
val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#9ED2EC"
tools:context=".MainActivity">
<com.skydoves.powerspinner.PowerSpinnerView
android:id="@+id/spinnerView_sido"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#27FF00"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:hint="도시 선택"
android:padding="10dp"
android:textColor="#FFFFFF"
android:textColorHint="#FFFFFF"
android:textSize="14.5sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/spinnerView_goo"
app:spinner_arrow_gravity="end"
app:spinner_arrow_tint="#FFDD00"
app:spinner_divider_color="#FFFFFF"
app:spinner_divider_show="true"
app:spinner_divider_size="0.4dp"
app:spinner_item_array="@array/sido_array"
app:spinner_item_height="46dp"
app:spinner_popup_animation="normal"
app:spinner_popup_background="#999999"
app:spinner_popup_elevation="14dp"
tools:ignore="HardcodedText,UnusedAttribute" />
<com.skydoves.powerspinner.PowerSpinnerView
android:id="@+id/spinnerView_goo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#38FF00"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:hint="지역 선택"
android:padding="10dp"
android:textColor="#999999"
android:textColorHint="#999999"
android:textSize="14.5sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/spinnerView_sido"
app:layout_constraintTop_toTopOf="parent"
app:spinner_arrow_gravity="end"
app:spinner_arrow_tint="#FFEB3B"
app:spinner_divider_color="#999999"
app:spinner_divider_show="true"
app:spinner_divider_size="0.4dp"
app:spinner_item_height="46dp"
app:spinner_popup_animation="normal"
app:spinner_popup_elevation="14dp"
tools:ignore="HardcodedText,UnusedAttribute" />
<ImageView
android:id="@+id/iv_face"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mise1" />
<TextView
android:id="@+id/tv_p10value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text=" - ㎍/㎥"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_face" />
<TextView
android:id="@+id/tv_p10grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text=""
android:textColor="#048578"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_p10value" />
<TextView
android:id="@+id/tv_cityname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="50dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="도시를 선택해 주세요."
android:textColor="#242323"
android:textSize="36sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/iv_face"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text=""
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_cityname" />
</androidx.constraintlayout.widget.ConstraintLayout>
package com.example.miseya
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.miseya.databinding.ActivityMainBinding
import com.skydoves.powerspinner.IconSpinnerAdapter
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
var items = mutableListOf<DustItem>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// setOnSpinnerItemSelectedListener: oldItem, oldIndex, NewIndex, NewItem 받음
// NewItem으로 선택한 텍스트(spinner 안에서 선택한 시/도 텍스트)가 communicateNetWork(setUpDustParameter()) 에 들어감
binding.spinnerViewSido.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
// setUpDustParameter는 (키, 값)으로 매칭된 HashMap형태의 데이터를 반환
// communicateNetWork는 이 데이터를 인자로 받아
communicateNetWork(setUpDustParameter(text))
}
// 구 값이 선택됐을 때 동작
binding.spinnerViewGoo.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
Log.d("miseya", "selectedItem: spinnerViewGoo selected > $text")
// 구이름(text)에 해당하는 item을 selectedItem으로 선언
var selectedItem = items.filter { f -> f.stationName == text }
Log.d("miseya", "selectedItem: sidoName > " + selectedItem[0].sidoName)
Log.d("miseya", "selectedItem: pm10Value > " + selectedItem[0].pm10Value)
binding.tvCityname.text = selectedItem[0].sidoName + " " + selectedItem[0].stationName
binding.tvDate.text = selectedItem[0].dataTime
binding.tvP10value.text = selectedItem[0].pm10Value + " ㎍/㎥"
when (getGrade(selectedItem[0].pm10Value)) {
1 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#9ED2EC"))
binding.ivFace.setImageResource(R.drawable.mise1)
binding.tvP10grade.text = "좋음"
}
2 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#D6A478"))
binding.ivFace.setImageResource(R.drawable.mise2)
binding.tvP10grade.text = "보통"
}
3 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#DF7766"))
binding.ivFace.setImageResource(R.drawable.mise3)
binding.tvP10grade.text = "나쁨"
}
4 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#BB3320"))
binding.ivFace.setImageResource(R.drawable.mise4)
binding.tvP10grade.text = "매우나쁨"
}
}
}
}
// communicateNetWork는 onCreate() 안의 메인 스레드 안에서 돌지 못함
// Why? -> 통신 도중에 렉이 걸리면 메인 화면이 멈춰버리고 화면 중지가 되고 에러가 뜸. 이걸 방지하려고 BG에서 돌게 지정
// -> 코루틴으로 돌게 함
private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
// 응답받는 데이터 = Retrofit(NetworkClient)의 dustNetwork를 통해 getDust()를 실행, 데이터 받아오기
val responseData = NetWorkClient.dustNetWork.getDust(param)
Log.d("Parsing Dust ::", responseData.toString())
val adapter = IconSpinnerAdapter(binding.spinnerViewGoo)
// 필요한 데이터만 빼서 List 형태로 따로 지정
// Ex) '서울' -> 서울에 해당하는 item들만 빼기
items = responseData.response.dustBody.dustItem!!
// 두 번째 스피너에 넣을 구 이름들 정의
val goo = ArrayList<String>()
items.forEach {
Log.d("add Item :", it.stationName)
goo.add(it.stationName)
}
// 코루틴의 별도 스레드에서 돌아가는 중이라 UI를 건드릴 수가 없음(여기서는 스피너)
// -> 아래 코드 따로 넣어줘야 함
runOnUiThread {
binding.spinnerViewGoo.setItems(goo)
}
}
// Request Parameter 생성
private fun setUpDustParameter(sido: String): HashMap<String, String> {
// (키, 값)으로 매칭된 HashMap형태의 데이터 반환
// 인증키 선언
val authKey = "YlgP80NwhNBwLETbohJrUha7ygmEO09Y35mxU/uyz0N90+HesPbErivbWW5/8bp6aNZxNP8HwPo9WokX3r6O8w=="
// 요청변수명과 동일해야 함
// 요청변수명과 value를 매칭하여 입력
return hashMapOf(
"serviceKey" to authKey,
"returnType" to "json",
"numOfRows" to "100",
"pageNo" to "1",
"sidoName" to sido,
"ver" to "1.0"
)
}
fun getGrade(value: String): Int {
val mValue = value.toInt()
var grade = 1
grade = if (mValue >= 0 && mValue <= 30) {
1
} else if (mValue >= 31 && mValue <= 80) {
2
} else if (mValue >= 81 && mValue <= 100) {
3
} else 4
return grade
}
}
★ 앱 아이콘 커스터마이징하는 방법 ★
1) res → New → Image Asset 클릭
2) Path에서 원하는 이미지 파일 선택
3) Resize, background 등 원하는 스타일로 커스터마이즈
4) 완료하면 mipmap → ic_launcher 파일에 반영되어 있음
'[개발] > Kotlin 활용 앱 개발' 카테고리의 다른 글
구글 AdMob(애드몹) 광고 연동하기 - 배너 광고 (1) | 2024.09.24 |
---|---|
Retrofit 실습 예제 (2) Youtube API 가져오기 (0) | 2024.07.31 |
Retrofit 개념 (0) | 2024.07.31 |
Google Map 가져오기 (0) | 2024.07.31 |
사용자 위치 얻기 (0) | 2024.07.31 |