리저브콜드브루 2025. 2. 25. 13:55
728x90
반응형

이전까지의 코드에서 API 호출된 응답(Response)에 옵저버를 붙여서 날씨 데이터를 업데이트했고 날씨 데이터에 옵저버를 붙여서 UI 업데이트가 되도록 만들었다.

  • LiveData(날씨 Response)를 다른 LiveData(날씨 Data)가 관찰하고, LiveData(날씨 Data) 를 UI 업데이트를 위해 관찰하는 구조에서 오류가 많았다

이 부분을 끊고자 많은 부분을 학습하고 수정했다

요점은 아래와 같다

  • API 호출에 대한 응답은 옵저버를 붙이지 않는다
  • 날씨 데이터값을 LiveData로써 업데이트는 유지
  • 액티비티에서 날씨 데이터에 옵저버를 붙이도록 한다

변경된 주요 클래스

 

Repository

  • API 응답값을 LiveData로 캐싱하던 부분을 삭제했다
class WeatherRepository {

    private val nowCastResponse = mutableMapOf<String, NcstWeatherResponse>()
    private val foreCastResponse = mutableMapOf<String, FcstWeatherResponse>()

    suspend fun fetchNowcastData(serviceKey: String, cityKey: String, nx: Int, ny: Int): NcstWeatherResponse? {
        return try {
            val (baseDate, baseTime) = WeatherUtils.getCurrentNcstBaseTime()
            val response: Response<NcstWeatherResponse> = RetrofitClient.instance.create(WeatherApi::class.java)
                .getWeather(serviceKey, baseDate = baseDate, baseTime = baseTime, nx = nx, ny = ny)

            if (response.isSuccessful) {
                response.body()?.let {
                    nowCastResponse[cityKey] = it // 캐싱
                    return it
                }
            }
            Log.e("WeatherRepository", "Nowcast API 요청 실패: ${response.errorBody()?.string()}")
            nowCastResponse[cityKey] // 기존 캐시 데이터 반환
        } catch (e: IOException) {
            Log.e("WeatherRepository", "네트워크 오류: ${e.message}")
            nowCastResponse[cityKey]
        } catch (e: HttpException) {
            Log.e("WeatherRepository", "HTTP 오류: ${e.message}")
            nowCastResponse[cityKey]
        }
    }

    suspend fun fetchForecastData(serviceKey: String, cityKey: String, nx: Int, ny: Int): FcstWeatherResponse? {
        return try {
            val (baseDate, baseTime) = WeatherUtils.getCurrentFcstBaseTime()
            val response: Response<FcstWeatherResponse> = RetrofitClient.instance.create(WeatherApi::class.java)
                .getForecast(serviceKey, baseDate = baseDate, baseTime = baseTime, fcstDate = baseDate, fcstTime = baseTime, nx = nx, ny = ny)

            if (response.isSuccessful) {
                response.body()?.let {
                    foreCastResponse[cityKey] = it // 캐싱
                    return it
                }
            }
            Log.e("WeatherRepository", "Forecast API 요청 실패: ${response.errorBody()?.string()}")
            foreCastResponse[cityKey]
        } catch (e: IOException) {
            Log.e("WeatherRepository", "네트워크 오류: ${e.message}")
            foreCastResponse[cityKey]
        } catch (e: HttpException) {
            Log.e("WeatherRepository", "HTTP 오류: ${e.message}")
            foreCastResponse[cityKey]
        }
    }
}

 

WeatherVieModel

  •  t1h, sky, pty 관련 날씨 데이터들을 WeatherData라는 데이터 클래스 하나로 통합 관리하도록 수정했다
data class WeatherData(
    val temperature: String = "N/A",
    val skyCondition: Int = 0,
    val precipitationType: Int = 1,
    val weatherIcon: Int = R.drawable.sunny_128
)
class WeatherViewModel(private val weatherRepository: WeatherRepository) : ViewModel() {

    private val _weatherDataMap = mutableMapOf<String, MutableLiveData<WeatherData>>()
    val weatherDataMap: MutableMap<String, MutableLiveData<WeatherData>> get() = _weatherDataMap

    fun getWeatherLiveData(cityKey: String): MutableLiveData<WeatherData> {
        return _weatherDataMap.getOrPut(cityKey) { MutableLiveData() }
    }


    fun fetchWeatherData(serviceKey: String, cityKey: String, nx: Int, ny: Int) {
        Log.d("WeatherViewModel", "Fetching weather data for $cityKey")
        viewModelScope.launch {
            val nowcastData = weatherRepository.fetchNowcastData(serviceKey, cityKey, nx, ny)
            val forecastData = weatherRepository.fetchForecastData(serviceKey, cityKey, nx, ny)

            if (nowcastData != null && forecastData != null) {
                val itemsNowcast = nowcastData.response?.body?.items?.item
                val itemsForecast = forecastData.response?.body?.items?.item

                val temperature = itemsNowcast?.find { it.category == "T1H" }?.obsrValue ?: "N/A"
                val precipitationType = itemsNowcast?.find { it.category == "PTY" }?.obsrValue?.toIntOrNull() ?: 0
                val skyCondition = itemsForecast?.find { it.category == "SKY" }?.fcstValue?.toIntOrNull() ?: 1
                val weatherIcon = WeatherUtils.getWeatherIcon128(skyCondition, precipitationType)

                Log.d("WeatherViewModel", "CityKey: $cityKey, Temp: $temperature, SKY: $skyCondition, PTY: $precipitationType, Icon: $weatherIcon")


                val updatedWeatherData = WeatherData(
                    temperature = temperature,
                    skyCondition = skyCondition,
                    precipitationType = precipitationType,
                    weatherIcon = weatherIcon
                )

                getWeatherLiveData(cityKey).postValue(updatedWeatherData)
            } else {
                Log.e("WeatherViewModel", "Failed to fetch weather data for $cityKey")
            }

        }
    }
}

변경된 액티비티 코드

 

MainActivity

  • 액티비티에서 UI 업데이트를 위한 옵저버를 붙인다
class MainActivity : AppCompatActivity() {
    private lateinit var weatherViewModel: WeatherViewModel

    private lateinit var locationDataStore: LocationDataStore
    private lateinit var locationProvider: LocationProvider

    private lateinit var mapController: MapController // 지도 관리 클래스 추가
    private lateinit var mapUIManager: MapUIManager

    private lateinit var textViewLocation: TextView // 위치를 표시할 텍스트뷰
    private lateinit var textViewTemperature: TextView //현재 위치의 현재 온도
    private lateinit var imageViewWeatherIcon: ImageView //현재 위치의 날씨아이콘
    private lateinit var mapView: MapView // 카카오맵 뷰
    private lateinit var searchButton : ImageButton

    companion object {
        private const val LOCATION_PERMISSION_REQUEST_CODE = 1000 //위치 권한 요청 코드
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //초기화
        initializeUI()
        initializeDependencies()
        setUIListener() // UI 리스너 등록

        //API 호출 부분
        checkLocationPermission() // 위치 권한 및 데이터 업데이트
        callFetchWeatherData()

        //UI 업데이트
        updateLocationUI()
        mapController.init{ kakaoMap ->
            mapUIManager = MapUIManager(this, kakaoMap, mapController, locationDataStore)
        }
       
    }

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

    override fun onPause() {
        super.onPause()
        mapController.onPause()
    }

    private fun initializeUI() {
        textViewLocation = findViewById(R.id.textView_location) //텍스트뷰 초기화
        textViewTemperature = findViewById(R.id.textView_temparature)
        imageViewWeatherIcon = findViewById(R.id.image_weather_icon)
        mapView = findViewById(R.id.map_view)
        searchButton = findViewById(R.id.searchButton)
    }

    private fun initializeDependencies() {

        locationDataStore = LocationDataStore(this)
        locationProvider = LocationProvider(this, locationDataStore)

        weatherViewModel = ViewModelProvider(this, WeatherViewModelFactory())[WeatherViewModel::class.java]
        mapController = MapController(this, mapView)
    }

    private fun checkLocationPermission() {
        if (locationProvider.checkLocationPermission()) {
            locationProvider.fetchLocation()
        } else {
            locationProvider.requestLocationPermission(LOCATION_PERMISSION_REQUEST_CODE)
        }
    }

    private fun setUIListener()
    {
        searchButton.setOnClickListener{
            val intent = Intent(this, SearchActivity::class.java)
            startActivity(intent)
            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
        }
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    private fun updateLocationUI() {
        lifecycleScope.launch {
            val latitude = locationDataStore.cur_latitude.first()
            val longitude = locationDataStore.cur_longitude.first()

            if (latitude != null && longitude != null) {
                Log.d("MainActivity", "위도: $latitude, 경도: $longitude")

                val geocoder = Geocoder(this@MainActivity, Locale.getDefault())

                geocoder.getFromLocation(latitude, longitude, 1) { addresses ->
                    if (addresses.isNotEmpty()) {
                        val address = addresses[0]
                        val city = address.locality ?: address.adminArea ?: "위치 불명"
                        textViewLocation.text = city
                        Log.d("MainActivity", "도시: $city")
                    } else {
                        textViewLocation.text = "위치 확인 불가"
                        Log.e("MainActivity", "Geocoder가 주소를 찾지 못했습니다.")
                    }
                }
            } else {
                textViewLocation.text = "위치 정보 없음"
            }
        }
    }


    // requestLocationPermission 콜백
    // 권한 요청 결과 처리
    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == LOCATION_PERMISSION_REQUEST_CODE &&
            grantResults.isNotEmpty() &&
            grantResults[0] == PackageManager.PERMISSION_GRANTED
        ) {
            locationProvider.fetchLocation() //권한이 허용된 경우 위치 가져오기
        } else {
            textViewLocation.text = "위치 권한이 필요합니다."
        }
    }

    private fun callFetchWeatherData() {
        this.lifecycleScope.launch {

            val latitude = locationDataStore.cur_latitude.first()
            val longitude = locationDataStore.cur_longitude.first()
            val cityKey = locationDataStore.locationKey.first()

            if (latitude != null && longitude != null && cityKey != null) {

                val (nx, ny) = WeatherUtils.LatLngToGridConverter.latLngToGrid(latitude, longitude)

                // 옵저버 등록
                registerWeatherUIObserver(cityKey)

                // 날씨 데이터를 가져옴
                weatherViewModel.fetchWeatherData(
                    serviceKey = "",
                    cityKey = cityKey,
                    nx = nx,
                    ny = ny
                )

            } else {
                Log.e("MainActivity", "위치 정보 없음. 날씨 데이터를 가져올 수 없습니다.")
            }
        }
    }

    private fun registerWeatherUIObserver(cityKey: String) {
        Log.d("MainActivity", "LiveData 옵저버 등록: $cityKey")

        if (weatherViewModel.getWeatherLiveData(cityKey).hasActiveObservers()) {
            Log.d("MainActivity", "이미 옵저버가 등록됨: $cityKey")
            return
        }

        weatherViewModel.getWeatherLiveData(cityKey).observe(this) { weatherData ->
            Log.d("MainActivity", "도시: $cityKey | 날씨 데이터 변경 감지됨: $weatherData")

            textViewTemperature.text = "${weatherData.temperature}°C"
            imageViewWeatherIcon.setImageResource(weatherData.weatherIcon)
            mapUIManager.updateWeatherLabel(weatherData.weatherIcon)
        }
    }
}

 

SearchActivity

MainActivity와 유사한 구조가 되도록 수정

class SearchActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: CityWeatherAdapter
    private val cityWeatherList = mutableListOf<CityWeatherInfo>()

    private lateinit var weatherViewModel: WeatherViewModel
    private lateinit var cityList: List<CityCallInfo> // 주요 도시 위도/경도 데이터

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_search)

        //초기화
        initializeUI()
        initializeDependencies()

        cityList = CityDataProvider.cityList

        //API 호출 부분
        loadCityWeatherData()
    }

    private fun initializeUI() {
        recyclerView = findViewById(R.id.recyclerView_search)
        adapter = CityWeatherAdapter(cityWeatherList)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter
    }

    private fun initializeDependencies(){
        weatherViewModel = ViewModelProvider(this, WeatherViewModelFactory())[WeatherViewModel::class.java]
    }

    private fun loadCityWeatherData() {
        for (city in cityList) {
            val cityKey = "${city.latitude}_${city.longitude}"

            // 도시 기본 데이터 추가 (초기 UI 표시)
            cityWeatherList.add(CityWeatherInfo(cityKey, city.name, R.drawable.sunny_128, "로딩 중..."))
            // RecyclerView 갱신
            adapter.updateList(cityWeatherList)

            // LiveData 옵저버 등록
            registerWeatherObserver(cityKey, city.name)

            // 날씨 데이터 가져오기
            fetchWeatherData(cityKey, city.name, city.latitude, city.longitude)
        }
    }

    private fun fetchWeatherData(cityKey: String, cityName: String, latitude: Double, longitude: Double) {

        val (nx, ny) = WeatherUtils.LatLngToGridConverter.latLngToGrid(latitude, longitude)

        lifecycleScope.launch {

            weatherViewModel.fetchWeatherData(
                serviceKey = "", // 기상청 API 키
                cityKey = cityKey,
                nx = nx,
                ny = ny
            )
        }

    }

    private fun registerWeatherObserver(cityKey: String, cityName: String) {
        Log.d("SearchActivity", "LiveData 옵저버 등록: $cityKey")

        if (weatherViewModel.getWeatherLiveData(cityKey).hasActiveObservers()) {
            Log.d("SearchActivity", "이미 옵저버가 등록됨: $cityKey")
            return
        }

        weatherViewModel.getWeatherLiveData(cityKey).observe(this) { weatherData ->
            Log.d("SearchActivity", "도시: $cityName ($cityKey) | 업데이트 감지됨: $weatherData")

            updateCityWeatherUI(cityKey, weatherData.temperature, weatherData.weatherIcon)
        }
    }

    private fun updateCityWeatherUI(cityKey: String, temperature: String, iconRes: Int) {
        val index = cityWeatherList.indexOfFirst { it.cityKey == cityKey }
        if (index != -1) {
            cityWeatherList[index] = cityWeatherList[index].copy(
                temperature = "$temperature°C",
                weatherIcon = iconRes
            )
            adapter.notifyItemChanged(index)
        }
    }

}
728x90
반응형