키워드 : RecyclerView, Retrofit, Spinner, API, DTO

API

API는 OpenWeatherAPI를 사용

3시간 간격 4일 (Forecast), 실시간 날씨 (Current)를 사용하였습니다.

 

 

Weather API - OpenWeatherMap

Please, sign up to use our fast and easy-to-work weather APIs. As a start to use OpenWeather products, we recommend our One Call API 3.0. For more functionality, please consider our products, which are included in professional collections.

openweathermap.org

Hourly Forecast 4 Days와 Current Weather Data 살펴보기

필수 파라미터 값

q(도시)

appid(api값)

Current Weather Data

Hourly Forecast 같은 경우 "list" 배열 내부에

main - temp - 온도

weather - icon - 아이콘

dt_txt - 시간

 

온도, 아이콘, 시간만으로 기상예보 (3시간, 4일)을 출력하겠습니다.

 

ForecastDTO (기상예보 뽑아올 데이터)

data class ForcecastDTO(
    //list는 배열이기 때문에 List<>로 선언 후 
    //list 내부에 있는 온도, 아이콘, 시간을 추출합니다
    @SerializedName("list") val list: List<ForcecastMain>
)

data class ForcecastMain(
    //val main, time은 객체 타입이기 때문에 Class로
    //val weather은 배열이기 때문에 List<>로 지정합니다.
    @SerializedName("main") val main: ForcecasTemp,
    @SerializedName("weather") val weather: List<ForcecasWeather>,
    @SerializedName("dt_txt")val time: String
)

data class ForcecasTemp(
   //temp - 온도
    val temp: Double
)

data class ForcecasWeather(
    //icon - 아이콘을 통해 openWeather API에서 제공하는
    //아이콘으로 기상예보를 표시합니다.
    val icon: String
)

 

ResultDTO (실시간 날씨 뽑아올 데이터)

data class RealtimeDTO (
    val weather: List<Weather>,
    val main: Main,
    val wind: Wind
)

data class Weather(
    val main: String?,
    val description: String?,
    val icon: String?
)

data class Main(
    val temp: Double?,
    val humidity: Int?,
    val temp_min: Double?,
    val temp_max: Double?
)

data class Wind(
    val speed: Double?,
)

 

WeatherAPI interface (필수 파라미터)

interface WeatherAPI {
    @GET("data/2.5/weather")
    fun getRealtime(
        @Query("q") q: String,
        @Query("appid") appid: String
    ): Call<RealtimeDTO>
//q : 도시명, appid : API키 모두 필수 파라미터입니다.
    @GET("data/2.5/forecast")
    fun getForecast(
        @Query("q") q: String,
        @Query("appid") appid: String,
        @Query("lang") lang: String
    ) : Call<ForcecastDTO>
}

베이스 url https://api.openweathermap.org

을 제외한 나머지를 @GET형식으로 불러옵니다.

GET 방식은 URL에 request 파라미터를 추가하는 방식입니다.

 

WeatherActivity.kt

class WeatherActivity : AppCompatActivity() {

    private lateinit var binding: ActivityWeatherBinding

    private val HOST_URL = "https://api.openweathermap.org/"
    private var cityName = "Seoul"
    private val API_KEY = "API키"


    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityWeatherBinding.inflate(layoutInflater)
        setContentView(binding.root)

        locationinit() //Spinner 도시 설정
        getRealtime() //실시간 날씨 가져오기
        getForecast() //3시간 기준 4일 날씨
        timeinit()   //실시간 시간 가져오기
        refreshButtoninit() //날씨 새로고침

    }
    //RecyclerView에 필요한 adapter 장착 및 가로 RecyclerView
    private fun setAdapter(dataList : ArrayList<ForcecastMain>) {
        val myAdapter = WeatherAdapter(this, dataList)
        binding.forecastRecyclerView.adapter = myAdapter
        binding.forecastRecyclerView.layoutManager = LinearLayoutManager(this@WeatherActivity, LinearLayoutManager.HORIZONTAL, false)
        binding.forecastRecyclerView.setHasFixedSize(false)
    }


	//DropDown Spinner로 지역을 선택하게 하고 선택과 동시에 
    //getRealtime(), timeinit(), getForecast()를 호출합니다.
    
    private fun locationinit() = with(binding) {
        val cityList = listOf("서울", "인천", "춘천", "강릉", "대전", "충청북도", "충청남도", "대구", "전주", "목포", "광주", "부산", "제주")
        val adapter = ArrayAdapter(this@WeatherActivity, R.layout.style_spinner, cityList)
        adapter.setDropDownViewResource(R.layout.style_spinner2)
        spinnerLocation.adapter = adapter

        spinnerLocation.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
            @RequiresApi(Build.VERSION_CODES.O)
            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
                when(position) {
                    0 -> {
                        cityName = "Seoul"
                        getRealtime()
                        timeinit()
                        getForecast()
                    }
                    1 -> {
                        cityName = "Incheon"
                        getRealtime()
                        timeinit()
                        getForecast()
                    }
                    ... 생략
                }
            }

            override fun onNothingSelected(parent: AdapterView<*>?) {

            }

        }

    }
    //실시간 날씨예보 (온도, 습도, 풍속 등)
    private fun getRealtime() = with(binding) {
        //retrofit Builder패턴 생성 및 url 지정, Gson Data 변환
        val retrofit = Retrofit.Builder()
            .baseUrl(HOST_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            
        //interface 지정 후 DTO를 통해 원하는 값 불러오기    
        val weatherAPI = retrofit.create(WeatherAPI::class.java)
        val call = weatherAPI.getRealtime(cityName, API_KEY)
        call.enqueue(object : Callback<RealtimeDTO> {
            @SuppressLint("SetTextI18n")
            override fun onResponse(call: Call<RealtimeDTO>, response: Response<RealtimeDTO>) {
                if(response.isSuccessful) {
                    //현재 온도, 최고온도, 최저 온도,
                    val now = (String.format("%.1f", response.body()!!.main.temp!! - 273))
                    val max = (String.format("%.1f",response.body()!!.main.temp_max!! - 273)) + "°C"
                    val min = (String.format("%.1f",response.body()!!.main.temp_min!! - 273)) + "°C"

                    temperatureTextView.text = now
                    maxvalueTextView.text = max
                    minvalueTextView.text = min
                    //습도 및 풍속
                    humidityTextView.text = response.body()!!.main.humidity.toString() + "%"
                    windspeedTextView.text = response.body()!!.wind.speed.toString() + "km/h"

                }
            }
			//실패 시 예외처리
            override fun onFailure(call: Call<RealtimeDTO>, t: Throwable) {
                Log.d("결과3", "실패! $t")
            }
        })
    }
    
	//3시간 기준 4일 일기예보
    private fun getForecast() {

        val retrofit = Retrofit.Builder()
            .baseUrl(HOST_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        val weatherAPI = retrofit.create(WeatherAPI::class.java)
        val call = weatherAPI.getForecast(cityName, API_KEY, "kr")
        call.enqueue(object: Callback<ForcecastDTO> {
            override fun onResponse(call: Call<ForcecastDTO>, response: Response<ForcecastDTO>) {
                if(response.isSuccessful) {
                    val body = response.body()
                    body?.let {
                        setAdapter(it.list as ArrayList<ForcecastMain>)
                    }
                }
            }

            override fun onFailure(call: Call<ForcecastDTO>, t: Throwable) {
                Log.d("포케스트 결과", "실패")
            }
        })
    }

	//실시간 시간을 불러오고, 시간에 따라 BackGround 변경
    @SuppressLint("SetTextI18n")
    @RequiresApi(Build.VERSION_CODES.O)
    private fun timeinit() = with(binding) {
        val today = LocalDate.now().toString()
        todayTextView.text = today

        val hour = LocalDateTime.now().hour.toString()
        val minute = LocalDateTime.now().minute.toString()

        nowTimeTextView.text = hour + ":" + minute

        if(hour.toInt() >= 12) {
            nowTimeTextView.text = "오후 " + hour + ":" + minute
        } else {
            nowTimeTextView.text = "오전 " + hour + ":" + minute
        }

        if(18 <= hour.toInt() || 0 == hour.toInt() || hour.toInt() <= 6) {
            backGround.setBackgroundResource(R.drawable.nightbackground)
            view1.setBackgroundResource(R.drawable.shape2)
            view2.setBackgroundResource(R.drawable.shape2)
            weatherImageView.setBackgroundResource(R.drawable.ic_night)
        } else {
            backGround.setBackgroundResource(R.color.moring)
            view1.setBackgroundResource(R.drawable.shape1)
            view2.setBackgroundResource(R.drawable.shape1)
            weatherImageView.setBackgroundResource(R.drawable.ic_afternoon)
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun refreshButtoninit() = with(binding) {
        refreshButton.setOnClickListener {
            progressBar.visibility = View.VISIBLE
            this@WeatherActivity.timeinit()
            getRealtime()
            progressBar.visibility = View.GONE
            Toast.makeText(this@WeatherActivity, "날씨가 갱신되었습니다.", Toast.LENGTH_SHORT).show()
        }
    }
}

 

WeatherAdapter.kt

class WeatherAdapter(private val context: Context, private val datas: ArrayList<ForcecastMain>): RecyclerView.Adapter<WeatherAdapter.ViewHolder>() {

    //WeatherAdapter와 item_list를 연결
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return WeatherAdapter.ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_forecast_list, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        //데이터를 순서대로 모든 값 바인딩
        holder.bind(datas[position], context)
    }

    override fun getItemCount() = datas.size //어탭터로 바인딩된 아이템 개수 반환
	
    //item_list에 있는 값과 Retrofit으로 불러온 값을 연결
    class ViewHolder(view: View?): RecyclerView.ViewHolder(view!!) {
        private val iconImage = view?.findViewById<ImageView>(R.id.iconImageView)
        private val realtime = view?.findViewById<TextView>(R.id.timeTextView)
        private val temp = view?.findViewById<TextView>(R.id.tempTextView)

        fun bind(data: ForcecastMain, context: Context){
            data.weather.forEach {
                val icon = it.icon
                val iconUrl = "http://openweathermap.org/img/w/" + icon + ".png"
                //Glide 이미지라이브러리를 통해 아이콘 불러옴
                Glide.with(context).load(iconUrl).into(iconImage!!)
            }
            temp?.text = (String.format("%.1f", data.main.temp - 273)) + "°C"

            val str = data.time
            val subString = str.substring(5 until 10)
            val subString2 = str.substring(11 until 16)
            realtime?.text = subString + "\n" + subString2
        }
    }
}
복사했습니다!