날씨 정보 불러오기 (OpenWeathermap API)

52610 단어 kotlinandroidandroid

Fragment 화면에 현 위치의 날씨 정보를 띄워보자!

xml

먼저 날씨 정보를 표시할 레이아웃은 다음과 같다. 날씨에 해당하는 아이콘 weather_ic, 현재 기온을 표시할 temperature_tv, 현재 날씨(ex. rain)를 표시할 weather_tv가 있다.

<RelativeLayout
    android:id="@+id/weather_tip_container"
    android:padding="8dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/weather_ic"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_centerVertical="true"
        android:layout_margin="@dimen/margin_8"
        android:src="@drawable/clear" />

    <LinearLayout
        android:id="@+id/weather_txt_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_margin="@dimen/margin_8"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/weather_ic">
        <TextView
            android:id="@+id/temperature_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="25sp"
            tools:text="00" />
        <TextView
            android:id="@+id/weather_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="Sunny"/>
    </LinearLayout>

</RelativeLayout>

AndroidManifest - permission

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

인터넷과 위치 정보를 가져오기 위한 권한을 추가한다

build.gradle

implementation 'com.loopj.android:android-async-http:1.4.11'
implementation "com.squareup.retrofit2:converter-gson:2.9.0"

( + 나는 More than one file was found with OS independent path 'META-INF/LICENSE.md' 이런식의 에러가 나서 packagingOptions도 추가했다. 참고 stackoverflow )

OpenWeather 가입

API를 사용해 날씨 정보를 불러오려면 OpenWeather 사이트에 가입해 개인 API KEY를 받아야 한다.

Fragment 전체

class StartFragment : Fragment() {

    companion object {
        const val API_KEY: String = "@%$@%@$!^*%@$@#!$$%"
        const val WEATHER_URL: String = "https://api.openweathermap.org/data/2.5/weather"
        const val MIN_TIME: Long = 5000
        const val MIN_DISTANCE: Float = 1000F
        const val WEATHER_REQUEST: Int = 102
    }
    
    private var binding: FragmentStartBinding?= null
    private lateinit var weatherState: TextView
    private lateinit var temperature: TextView
    private lateinit var weatherTip: TextView
    private lateinit var weatherIcon: ImageView

    private lateinit var mLocationManager: LocationManager
    private lateinit var mLocationListener: LocationListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val fragmentBinding = FragmentStartBinding.inflate(inflater, container, false)
        binding = fragmentBinding
        return fragmentBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding?.startFragment = this
        binding?.apply {
            temperature = temperatureTv
            weatherState = weatherTv
            weatherTip = weatherTipTv
            weatherIcon = weatherIc
        }

    }

    override fun onResume() {
        super.onResume()
        getWeatherInCurrentLocation()
    }

    private fun getWeatherInCurrentLocation(){
        mLocationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager

        mLocationListener = LocationListener { p0 ->
            val params: RequestParams = RequestParams()
            params.put("lat", p0.latitude)
            params.put("lon", p0.longitude)
            params.put("appid", Companion.API_KEY)
            doNetworking(params)
        }


        if (ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(requireActivity(), arrayOf<String>(Manifest.permission.ACCESS_FINE_LOCATION), WEATHER_REQUEST)
            return
        }
        mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, MIN_TIME, MIN_DISTANCE, mLocationListener)
        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_TIME, MIN_DISTANCE, mLocationListener)
    }


    private fun doNetworking(params: RequestParams) {
        var client = AsyncHttpClient()

        client.get(WEATHER_URL, params, object: JsonHttpResponseHandler(){
            override fun onSuccess(
                statusCode: Int,
                headers: Array<out Header>?,
                response: JSONObject?
            ) {
                val weatherData = WeatherData().fromJson(response)
                if (weatherData != null) {
                    updateWeather(weatherData)
                }
            }

        })
    }

    private fun updateWeather(weather: WeatherData) {
        temperature.setText(weather.tempString+" ℃")
        weatherState.setText(weather.weatherType)
        val resourceID = resources.getIdentifier(weather.icon, "drawable", activity?.packageName)
        weatherIcon.setImageResource(resourceID)
    }

    override fun onPause() {
        super.onPause()
        if(mLocationManager!=null){
            mLocationManager.removeUpdates(mLocationListener)
        }
    }
}
  • API_KEY에는 회원가입 후 발급 된 개인 API Key를 입력한다.

 

날씨 불러오기

getWeatherInCurrentLocation()

바인딩등 날씨 불러오기에 중요하지 않은 부분은 넘어가고 onResume()에서 호출되는 getWeatherInCurrentLocation()부터 보자.

    private fun getWeatherInCurrentLocation(){
        mLocationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager

        mLocationListener = LocationListener { p0 ->
            val params: RequestParams = RequestParams()
            params.put("lat", p0.latitude)
            params.put("lon", p0.longitude)
            params.put("appid", API_KEY)
            doNetworking(params)
        }
        
        if (ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(requireActivity(), arrayOf<String>(Manifest.permission.ACCESS_FINE_LOCATION), WEATHER_REQUEST)
            return
        }
        mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, MIN_TIME, MIN_DISTANCE, mLocationListener)
        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_TIME, MIN_DISTANCE, mLocationListener)
    }
  • params에 정보 요청에 필요한 파라미터를 담아 doNetworking()을 호출한다.
  • 권한이 허용되지 않았다면 ActivityCompat.requestPermissions이 호출되면서 위치 액세스 허용을 묻는 창이 뜬다.
  • LocationManager는 MIN_TIME이나 MIN_DISTANCE가 지나면 정보 업데이트를 요청한다.

 

doNetworking()

    private fun doNetworking(params: RequestParams) {
        var client = AsyncHttpClient()

        client.get(WEATHER_URL, params, object: JsonHttpResponseHandler(){
            override fun onSuccess(
                statusCode: Int,
                headers: Array<out Header>?,
                response: JSONObject?
            ) {
                val weatherData = WeatherData().fromJson(response)
                if (weatherData != null) {
                    updateWeather(weatherData)
                }
            }
        })
    }
  • RequestParams에 담긴 파라미터를 이용해 JSONObjectresponse를 받아온다. 그 다음 WeatherData에 전달해 이곳에 담긴 정보로 UI를 업데이트 하게된다.

 

updateWeather()

WeatherData 클래스를 보기에 앞서 WeatherData 클래스를 거쳐서 날씨 정보가 weatherData에 담기는데, updateWeather()는 이것을 전달받아 뷰를 업데이트 한다.

    private fun updateWeather(weather: WeatherData) {
        temperature.setText(weather.tempString+" ℃")
        weatherState.setText(weather.weatherType)
        val resourceID = resources.getIdentifier(weather.icon, "drawable", activity?.packageName)
        weatherIcon.setImageResource(resourceID)
    }
  • drawable에는 날씨 분류에 해당하는 아이콘 이미지가 있다.

 

WeatherData

이곳에서 response에 담긴 정보에서 필요한 정보를 가져오고, 그 정보를 바탕으로 weather condition에 해당하는 아이콘을 선택한다.

API response가 어떤 형태로 되어있는지는 여기(Weather fields in API response)에서 확인할 수 있다.
사이트의 예시를 보면 날씨와 기온 외에도 최저기온, 최고기온, 습도 등 다양한 정보를 가져올 수 있다는 것을 알 수 있다.

Weather condition code는 여기에서 확인할 수 있다.

class WeatherData{

    lateinit var tempString: String
    lateinit var icon: String
    lateinit var weatherType: String
    private var weatherId: Int = 0
    private var tempInt: Int =0

    fun fromJson(jsonObject: JSONObject?): WeatherData? {
        try{
            var weatherData = WeatherData()
            weatherData.weatherId = jsonObject?.getJSONArray("weather")?.getJSONObject(0)?.getInt("id")!!
            weatherData.weatherType = jsonObject.getJSONArray("weather").getJSONObject(0).getString("main")
            weatherData.icon = updateWeatherIcon(weatherData.weatherId)
            val roundedTemp: Int = (jsonObject.getJSONObject("main").getDouble("temp")-273.15).toInt()
            weatherData.tempString = roundedTemp.toString()
            weatherData.tempInt = roundedTemp
            return weatherData
        }catch (e: JSONException){
            e.printStackTrace()
            return null
        }
    }

    private fun updateWeatherIcon(condition: Int): String {
        if (condition in 200..299) {
            return "thunderstorm"
        } else if (condition in 300..499) {
            return "lightrain"
        } else if (condition in 500..599) {
            return "rain"
        } else if (condition in 600..700) {
            return "snow"
        } else if (condition in 701..771) {
            return "fog"
        } else if (condition in 772..799) {
            return "overcast"
        } else if (condition == 800) {
            return "clear"
        } else if (condition in 801..804) {
            return "cloudy"
        } else if (condition in 900..902) {
            return "thunderstorm"
        }
        if (condition == 903) {
            return "snow"
        }
        if (condition == 904) {
            return "clear"
        }
        return if (condition in 905..1000) {
            "thunderstorm"
        } else "dunno"

    }
    
}
  • weatherId에는 정수 형태의 weather condition code가 담기는데 여기에서 각 코드의 의미를 알 수 있다. 예를 들어 500은 Rain 카테고리에 해당하기 때문에 updateWeatherIcon()"rain"을 반환한다.
    반환된 문자열은 대응하는 아이콘을 찾는데 사용된다.
  • 기온은 double 타입이기 때문에 사용자가 보기 좋게 정수 타입으로 바꿔준 후 변수에 담는다.

좋은 웹페이지 즐겨찾기