You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

天气应用:将位置与DarkSky数据获取逻辑迁移至Repository

嘿,看起来你已经搭好了Room+ViewModel+LiveData的基础架构,现在想把Fragment里的位置和天气获取逻辑迁移到Repository,实现「先查本地缓存,无数据再走网络」的核心逻辑对吧?我帮你梳理下具体的实现步骤和代码示例~

核心思路梳理

你的Repository需要承担这几个核心职责:

  • 优先从Room数据库读取缓存的天气数据
  • 若本地无有效数据,先通过GoogleFusedLocationClient获取当前位置
  • 用位置坐标请求DarkSky的天气预报接口
  • 将网络返回的数据存入Room数据库持久化
  • 通过LiveData把「加载中/成功/失败」的状态和数据统一暴露给ViewModel

第一步:改造Repository类

Repository需要依赖Room Dao、Retrofit天气服务、GoogleFusedLocationClient,建议用构造函数注入这些依赖,符合依赖倒置原则。这里用Coroutines配合LiveData处理异步逻辑:

class WeatherRepository(
    private val weatherDao: WeatherDao,
    private val darkSkyService: DarkSkyService,
    private val fusedLocationClient: FusedLocationProviderClient,
    private val context: Context // 用于位置权限检查
) {
    // 对外暴露的天气数据LiveData,封装了状态
    fun getWeatherData(): LiveData<Resource<Weather>> {
        return liveData(Dispatchers.IO) {
            // 先发送加载状态
            emit(Resource.Loading)
            
            // 1. 检查本地缓存
            val localWeather = weatherDao.getLatestWeather()
            if (localWeather != null) {
                emit(Resource.Success(localWeather))
                // 可选:如果缓存过期(比如超过1小时),可以后台静默刷新数据
                if (isCacheExpired(localWeather.timestamp)) {
                    refreshWeatherData()
                }
                return@liveData
            }
            
            // 2. 本地无缓存,获取当前位置
            val location = try {
                getCurrentLocation()
            } catch (e: Exception) {
                emit(Resource.Error("获取位置失败:${e.message}"))
                return@liveData
            }
            
            // 3. 请求网络天气数据
            try {
                val remoteWeatherResponse = darkSkyService.getWeather(
                    latitude = location.latitude,
                    longitude = location.longitude
                )
                
                if (remoteWeatherResponse.isSuccessful) {
                    remoteWeatherResponse.body()?.let { remoteWeather ->
                        // 4. 存入本地数据库
                        weatherDao.insertWeather(remoteWeather.apply {
                            timestamp = System.currentTimeMillis() // 添加缓存时间戳
                        })
                        emit(Resource.Success(remoteWeather))
                    } ?: emit(Resource.Error("网络返回数据为空"))
                } else {
                    emit(Resource.Error("请求失败:${remoteWeatherResponse.message()}"))
                }
            } catch (e: IOException) {
                emit(Resource.Error("网络异常:${e.message}"))
            }
        }
    }

    // 封装位置获取的异步逻辑
    private suspend fun getCurrentLocation(): Location {
        return suspendCoroutine { continuation ->
            // 先检查位置权限
            if (ContextCompat.checkSelfPermission(
                    context,
                    Manifest.permission.ACCESS_FINE_LOCATION
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                continuation.resumeWithException(SecurityException("位置权限未授予"))
                return@suspendCoroutine
            }
            
            fusedLocationClient.lastLocation.addOnSuccessListener { location ->
                location?.let { continuation.resume(it) }
                    ?: continuation.resumeWithException(Exception("无法获取当前位置"))
            }.addOnFailureListener {
                continuation.resumeWithException(it)
            }
        }
    }

    // 辅助方法:判断缓存是否过期(示例:超过1小时)
    private fun isCacheExpired(timestamp: Long): Boolean {
        return System.currentTimeMillis() - timestamp > 3600000
    }

    // 手动刷新数据的方法(供ViewModel调用)
    private suspend fun refreshWeatherData() {
        // 逻辑和上面的网络请求部分一致,这里可以复用代码
        // 省略重复代码...
    }
}

// 封装数据状态的密封类,方便UI层处理不同状态
sealed class Resource<T> {
    object Loading : Resource<Nothing>()
    data class Success<T>(val data: T) : Resource<T>()
    data class Error<T>(val message: String) : Resource<T>()
}

第二步:简化ViewModel

ViewModel现在只需要持有Repository实例,调用其方法并暴露LiveData给Fragment,不需要处理任何业务逻辑:

class WeatherViewModel(
    private val repository: WeatherRepository
) : ViewModel() {
    // 对外暴露的天气数据LiveData
    val weatherData: LiveData<Resource<Weather>> = repository.getWeatherData()

    // 手动刷新数据的方法(供Fragment调用,比如下拉刷新)
    fun refreshWeather() {
        viewModelScope.launch {
            repository.refreshWeatherData()
        }
    }
}

第三步:极简Fragment实现

Fragment现在只需要做两件事:请求位置权限(如果未授予)、观察ViewModel的LiveData并更新UI,彻底剥离业务逻辑:

class WeatherFragment : Fragment() {
    private lateinit var viewModel: WeatherViewModel
    private val LOCATION_PERMISSION_REQUEST_CODE = 1001

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)
        return inflater.inflate(R.layout.fragment_weather, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 先检查位置权限
        checkLocationPermission()

        // 观察天气数据状态
        viewModel.weatherData.observe(viewLifecycleOwner) { resource ->
            when (resource) {
                is Resource.Loading -> {
                    progressBar.visibility = View.VISIBLE
                }
                is Resource.Success -> {
                    progressBar.visibility = View.GONE
                    updateWeatherUI(resource.data)
                }
                is Resource.Error -> {
                    progressBar.visibility = View.GONE
                    Toast.makeText(context, resource.message, Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    // 位置权限检查与请求
    private fun checkLocationPermission() {
        if (ContextCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissions(
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                LOCATION_PERMISSION_REQUEST_CODE
            )
        }
    }

    // 权限请求结果回调
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限授予,触发数据加载
                viewModel.refreshWeather()
            } else {
                Toast.makeText(context, "需要位置权限才能获取天气", Toast.LENGTH_SHORT).show()
            }
        }
    }

    // 更新UI的方法
    private fun updateWeatherUI(weather: Weather) {
        temperatureTv.text = getString(R.string.temperature_format, weather.temperature)
        conditionTv.text = weather.condition
        // 其他UI更新逻辑...
    }
}

几个关键注意点
  • 权限处理:位置权限必须在Fragment/Activity中请求,因为权限请求需要与组件绑定,不要在Repository中处理权限逻辑
  • 缓存策略:可以给Weather实体添加timestamp字段,判断缓存是否过期,实现「缓存优先+后台静默刷新」的体验
  • 依赖注入:如果使用Hilt/Dagger,建议用依赖注入替代手动创建Repository实例,减少代码耦合
  • 错误处理:通过Resource密封类统一处理加载、成功、失败状态,UI层可以更优雅地响应不同场景

内容的提问来源于stack exchange,提问作者Suleyman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:43:02