ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] 미세먼지 앱 (공공 데이터 API) + Retrofit
    안드로이드 2024. 5. 1. 15:35

    ✏️ TIL(Today I Learned)

    도시를 선택하면 그에 해당하는 데이터를 지역 선택 스피너에 받아와서 선택을 할 수 있다. 

     

    선택이 완료되면, TextView에 지역명이 표기되면서 미세먼지 농도와 상태가 이모티콘, 텍스트, 배경색으로 표시된다.

     

     

     

    API 사용하기 위해 공공데이터포털로 들어가서, 활용신청을 한다.

     

    그럼 바로 승인이 된 것을 확인할 수 있다.

     

    이를 클릭하여, 개발계정 상세 보기로 들어간다.

    그리고 미리보기를 클릭하여 데이터의 json을 확인해 볼 수 있다.

     

    아래와 같은 형식이다.

    {
      "response": {
        "body": {
          "totalCount": 0,
          "items": [],
          "pageNo": 1,
          "numOfRows": 100
        },
        "header": {
          "resultMsg": "NORMAL_CODE",
          "resultCode": "00"
        }
      }
    }

     

     


     

    💻 코드

    개발하기에 앞서, http통신을 위한 인터넷 사용권한을 AndroidManifest.xml에 추가한다.

    그리고 <application> 태그에 android:networkSecurityConfig 속성을 추가한다.

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <uses-permission android:name="android.permission.INTERNET"/>
    
        <application
            android:networkSecurityConfig="@xml/network_security_config"

     

    앱의 네트워크 보안 구성을 수정하여 apis.data.go.kr에 대해 평문 트래픽을 특별히 허용하기 위함이다.

    (최신 안드로이드 버전은 기본적으로 보안상의 이유로 평문 트래픽을 허용하지 않는다.)

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config cleartextTrafficPermitted="true">
            <domain includeSubdomains="true">apis.data.go.kr</domain>
        </domain-config>
    </network-security-config>

     

     

    그 다음 위에서 확인한 json을 data class형태로 바꾸기 위해, 이에 맞는 DTO를 만들어준다.

    변수명은 원래 서버에서 사용하는 값과 똑같이 작성해야 된다.

    data class Dust(val response: DustResponse)
    
    data class DustResponse(
        @SerializedName("body")
        val dustBody: DustBody,
        @SerializedName("header")
        val dustHeader: DustHeader
    )
    
    data class DustBody(
        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
    )

     

     

    시도명을 검색조건으로 할 것이므로 아래의 사진을 참고한다.

     

    Retrofit 네트워크 인터페이스를 통해, 시도별 실시간 측정 정보를 얻어온다.

    코루틴(Coroutine)을 사용하여 비동기적으로 API를 호출한다.

    interface NetWorkInterface {
        @GET("getCtprvnRltmMesureDnsty") //시도별 실시간 측정 정보 조회 주소
        suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
    }

     

     

    그다음, NetWorkClient 객체를 만들어 Retrofit 라이브러리를 사용하여,

    네트워크 요청을 구성하고 실행하는 데 필요한 구성 요소들을 정의한다.

     

    createOkHttpClient()는 네트워크 요청을 수행할 때 사용될 OkHttpClient 인스턴스를 생성한다.

    • HttpLoggingInterceptor는 로깅 인터셉터를 사용하여 요청과 응답에 대한 로그를 제공한다.
      앱이 디버그 모드인 경우 요청의 바디 내용을 모두 로깅하고, 아닐 경우 로깅하지 않는다.
    • 타임아웃 설정하여, 서버로부터 응답을 기다리는 최대 시간을 정의한다.

    dustRetrofit는 Retrofit.Builder()를 사용하여 Retrofit 인스턴스를 생성한다. 이는 API 호출을 수행하는 데 사용된다.

    • baseUrl(DUST_BASE_URL): 모든 네트워크 요청의 기본 URL로 설정한다.
    • addConverterFactory(GsonConverterFactory.create()): Gson 컨버터 팩토리를 추가하여 JSON 응답을 Kotlin 객체로 자동 변환할 수 있다.
    • client(createOkHttpClient()): 위에서 정의한 OkHttpClient 인스턴스를 HTTP 클라이언트로 사용한다.

    dustNetWork는 NetWorkInterface 인터페이스를 구현하는 객체를 생성한다.
    이 객체는 Retrofit이 제공하는 동적 프록시 기능을 통해 생성되며, 이 인터페이스에 정의된 메서드를 호출하면 자동으로 HTTP 요청이 구성되고 실행된다.

    object NetWorkClient {
        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
    
            return OkHttpClient.Builder()
                .connectTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .writeTimeout(20, TimeUnit.SECONDS)
                .addNetworkInterceptor(interceptor)
                .build()
        }
    
        private val dustRetrofit = Retrofit.Builder()
            .baseUrl(DUST_BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(
                createOkHttpClient()
            ).build()
    
        val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)
    }

     

     

    마지막으로, MainActivity에서 미세먼지 데이터를 받아와서 화면에 표시한다.

    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)
    
            binding.spinnerViewSido.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
                communicateNetWork(setUpDustParameter(text))
            }
    
            binding.spinnerViewGoo.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
                var selectedItem = items.filter { f -> f.stationName == text }
    
                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.image_mise1)
                        binding.tvP10grade.text = "좋음"
                    }
    
                    2 -> {
                        binding.mainBg.setBackgroundColor(Color.parseColor("#D6A478"))
                        binding.ivFace.setImageResource(R.drawable.image_mise2)
                        binding.tvP10grade.text = "보통"
                    }
    
                    3 -> {
                        binding.mainBg.setBackgroundColor(Color.parseColor("#DF7766"))
                        binding.ivFace.setImageResource(R.drawable.image_mise3)
                        binding.tvP10grade.text = "나쁨"
                    }
    
                    4 -> {
                        binding.mainBg.setBackgroundColor(Color.parseColor("#BB3320"))
                        binding.ivFace.setImageResource(R.drawable.image_mise4)
                        binding.tvP10grade.text = "매우나쁨"
                    }
                }
            }
        }
    
        private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
            val responseData = NetWorkClient.dustNetWork.getDust(param)
            items = responseData.response.dustBody.dustItem!!
    
            val goo = ArrayList<String>()
            items.forEach {
                Log.d("add Item :", it.stationName)
                goo.add(it.stationName)
            }
    
            runOnUiThread {
                binding.spinnerViewGoo.setItems(goo)
            }
        }
    
        private fun setUpDustParameter(sido: String): HashMap<String, String> {
            val authKey = getString(R.string.key)
    
            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
        }
    }
Designed by Tistory.