天气应用:将位置与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
相关产品推荐
相关产品推荐

