news 2026/10/7 4:11:05

Android Studio天气App开发:定位+Retrofit+ViewBinding实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android Studio天气App开发:定位+Retrofit+ViewBinding实战

简介:本资源是一套基于Android Studio开发的天气预报小程序完整源码工程,面向Android初学者与移动开发实践者,帮助掌握网络请求、API对接、UI动态绑定等核心开发技能。压缩包共1260个文件,主体包含509个flat资源文件、194个dex字节码、192个class类文件、120个json配置与响应数据、76个xml布局及配置文件、33个java业务逻辑源码,以及配套的png图标、gradle构建脚本和apk安装包等,整体大小为9.76MB,结构完整,可直接导入Android Studio运行调试。已有3876人学习下载,资源涵盖从项目初始化、Retrofit网络封装、OpenWeatherMap接口调用,到天气数据解析、UI实时更新与错误处理的全流程实现,代码注释清晰,关键模块(如WeatherApiService接口定义、MainActivity中enqueue异步回调、温度/城市名控件绑定)均具典型教学价值,适合动手复现并拓展多城切换、下拉刷新、本地缓存等功能。

1. Android Studio 实现天气预报小程序:不是微信小程序,是原生 Android 应用,源码可跑、接口可换、UI 可改

很多人搜“AndroidStudio实现天气预报小程序源码”,第一反应是点开就 expecting 微信开发者工具、wxml + wxss + js 三件套——但标题里写的是Android Studio,不是微信小程序开发环境。这是个典型的术语混淆:国内语境下,“小程序”常被泛化成“轻量级应用”,而本项目本质是基于 Android SDK 的原生 Java/Kotlin 桌面级天气 App,运行在真实手机上,调用系统权限(定位、网络)、使用 Material Design 组件、打包成.apk安装包。它不依赖任何第三方平台(如微信、支付宝容器),也不走小程序审核流程,适合课程设计、毕设原型、IoT 设备配套屏显或嵌入式 Android 系统的本地服务模块。核心能力包括:GPS/网络定位自动获取城市 → 调用免费天气 API(如和风天气、OpenWeatherMap)→ 解析 JSON 响应 → 渲染实时温度、湿度、风速、未来3天预报、天气图标(SVG 或 PNG)、刷新状态提示。新手能 2 小时跑通基础版,熟手可快速接入高精度气象数据源或离线缓存策略。本文所有代码、配置、接口调试步骤均基于 Android Studio Giraffe(2022.3.1)及 AGP 8.1.0 验证,适配 Android 10(API 29)至 Android 14(API 34),无 Gradle 插件冲突,无模拟器兼容性玄学问题。


2. 从零搭建工程结构:创建空项目、配置网络与定位权限、引入 Retrofit + Gson

2.1 新建 Empty Activity 项目并确认 Gradle 版本链

打开 Android Studio → New Project → Select "Empty Activity" → Package name 设为com.example.weatherapp(避免中文或特殊字符)→ Minimum SDK 选API 21 (Android 5.0)(覆盖 95% 设备,且支持现代 HTTP 客户端)。创建后,检查gradle/wrapper/gradle-wrapper.properties中distributionUrl是否为https\://services.gradle.org/distributions/gradle-8.0-bin.zip;再确认项目根目录build.gradle(注意是project-level,非 module-level)中plugins { id 'com.android.application' version '8.1.0'已声明。若版本不匹配,强制同步会报Could not resolve com.android.tools.build:gradle错误——这是新手最常卡住的第一步,别跳过。

// app/build.gradle(module-level) plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' // 若选 Kotlin } android { namespace 'com.example.weatherapp' compileSdk 34 defaultConfig { applicationId "com.example.weatherapp" minSdk 21 targetSdk 34 // 必须 ≥ 33 才能申请精确位置权限 versionCode 1 versionName "1.0" } // 关键:启用 viewBinding,避免 findViewById 冗余代码 buildFeatures { viewBinding true } }

提示:targetSdk 34是硬性要求。Android 12+ 对后台定位、网络明文请求有强限制,targetSdk < 33会导致ACCESS_FINE_LOCATION权限申请失败或Cleartext HTTP traffic not permitted异常。

2.2 声明必要权限并处理运行时授权逻辑

天气 App 必须获取用户位置以自动定位城市,因此需在app/src/main/AndroidManifest.xml中声明:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- Android 12+ 需额外声明 foreground service 权限(仅当需后台持续定位) --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

但声明 ≠ 自动获得。Android 6.0+ 要求运行时动态申请。我们在MainActivity.kt的onCreate()后插入权限检查:

private fun requestLocationPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), LOCATION_PERMISSION_REQUEST_CODE) } else { fetchWeatherByLocation() // 权限已授,直接获取天气 } } 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) { fetchWeatherByLocation() } else { Toast.makeText(this, "定位权限被拒绝,无法获取当前位置天气", Toast.LENGTH_LONG).show() } } }

参数说明:LOCATION_PERMISSION_REQUEST_CODE是自定义整型常量(如1001),用于onRequestPermissionsResult回调时区分不同权限请求。不要用0或负数,易与系统值冲突。

2.3 集成 Retrofit + Gson 处理网络请求与 JSON 解析

天气数据必走 HTTP 请求,手动写HttpURLConnection易出错且难维护。Retrofit 是工业级标准,Gson 负责反序列化。在app/build.gradle的dependencies块中添加:

implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // Retrofit 核心 implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // OkHttp 日志拦截器(调试必备) implementation 'com.squareup.okhttp3:logging-interceptor:4.11.3' // Lifecycle 绑定(防止内存泄漏) implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'

同步后,在app/src/main/java/com/example/weatherapp/下新建api/WeatherApiService.kt:

interface WeatherApiService { @GET("v2/weather/now") suspend fun getNowWeather( @Query("key") key: String, @Query("location") location: String ): Response<WeatherResponse> @GET("v2/weather/3d") suspend fun get3DayForecast( @Query("key") key: String, @Query("location") location: String ): Response<ForecastResponse> } // 单例 Retrofit 实例 object RetrofitClient { private const val BASE_URL = "https://devapi.qweather.com/" // 和风天气免费版地址 private val logging = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY // 调试时开,上线关闭 } private val client = OkHttpClient.Builder() .addInterceptor(logging) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build() val apiService: WeatherApiService by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() .create(WeatherApiService::class.java) } }

注意:BASE_URL必须以/结尾,否则@GET("v2/weather/now")会拼成https://devapi.qweather.comv2/weather/now(少斜杠)。这是 Retrofit 初学者踩坑率最高的 URL 拼接错误。


3. 设计数据模型与 UI 布局:解析 JSON 结构、用 ViewBinding 构建响应式界面

3.1 根据和风天气 API 文档定义 Kotlin 数据类

和风天气免费版返回 JSON 结构如下(精简):

{ "code": "200", "now": { "obsTime": "2024-05-20T14:22+08:00", "temp": "26", "feelsLike": "28", "textDay": "多云", "wind360": "90", "windDir": "东风", "windScale": "3", "humidity": "65", "precip": "0.0", "pressure": "1005" } }

对应创建data/WeatherResponse.kt:

data class WeatherResponse( val code: String, val now: NowWeather ) data class NowWeather( val obsTime: String, val temp: String, // 温度字符串,非数字(API 设计如此) val feelsLike: String, // 体感温度 val textDay: String, // 白天天气描述 val windDir: String, // 风向 val windScale: String, // 风力等级 val humidity: String, // 湿度百分比 val pressure: String // 气压 hPa )

关键细节:temp字段是"26"而非26,所以必须声明为String。若误写成Int,Gson 解析时会抛JsonParseException,App 直接 Crash。同理humidity是"65",不是65。这是免费 API 的典型设计,务必按实际 JSON 类型建模。

3.2 编写 activity_main.xml 布局文件(Material Design 风格)

使用ConstraintLayout实现响应式布局,重点元素:顶部城市名 TextView、中央大温度 Textview、天气图标 ImageView、下方参数网格(湿度/风力/气压)。关键点:所有控件 ID 必须唯一,且与 ViewBinding 生成类字段名一致。

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/tvCityName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="北京市" android:textSize="24sp" android:textStyle="bold" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="32dp" /> <TextView android:id="@+id/tvTemperature" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="26°" android:textSize="72sp" android:textStyle="bold" app:layout_constraintTop_toBottomOf="@id/tvCityName" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="24dp" /> <ImageView android:id="@+id/ivWeatherIcon" android:layout_width="80dp" android:layout_height="80dp" android:src="@drawable/ic_cloudy" app:layout_constraintTop_toBottomOf="@id/tvTemperature" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="16dp" /> <!-- 参数网格:3列2行 --> <androidx.gridlayout.widget.GridLayout android:id="@+id/gridParams" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/ivWeatherIcon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="32dp" app:columnCount="3" app:rowCount="2"> <TextView android:id="@+id/tvHumidityLabel" android:layout_width="0dp" android:layout_height="wrap_content" android:text="湿度" android:gravity="center" app:layout_columnWeight="1" /> <TextView android:id="@+id/tvWindLabel" android:layout_width="0dp" android:layout_height="wrap_content" android:text="风力" android:gravity="center" app:layout_columnWeight="1" /> <TextView android:id="@+id/tvPressureLabel" android:layout_width="0dp" android:layout_height="wrap_content" android:text="气压" android:gravity="center" app:layout_columnWeight="1" /> <TextView android:id="@+id/tvHumidityValue" android:layout_width="0dp" android:layout_height="wrap_content" android:text="65%" android:gravity="center" app:layout_columnWeight="1" /> <TextView android:id="@+id/tvWindValue" android:layout_width="0dp" android:layout_height="wrap_content" android:text="3级" android:gravity="center" app:layout_columnWeight="1" /> <TextView android:id="@+id/tvPressureValue" android:layout_width="0dp" android:layout_height="wrap_content" android:text="1005hPa" android:gravity="center" app:layout_columnWeight="1" /> </androidx.gridlayout.widget.GridLayout> <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/gridParams" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="32dp" android:visibility="gone" /> </androidx.constraintlayout.widget.ConstraintLayout>

注意:GridLayout在 AndroidX 中已迁移到androidx.gridlayout.widget.GridLayout,旧android.widget.GridLayout已废弃。若引用错误,编译时报Class not found。

3.3 在 MainActivity 中绑定 View 并填充数据

启用 ViewBinding 后,MainActivity.kt不再用setContentView(R.layout.activity_main),而是:

class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding // 自动生成的 Binding 类 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) // 关键:inflate 替代 setContentView setContentView(binding.root) // root 是 ConstraintLayout 根节点 // 初始化 UI 状态 binding.progressBar.visibility = View.GONE binding.tvCityName.text = "定位中..." binding.tvTemperature.text = "--°" requestLocationPermission() // 触发权限申请 } private fun updateUI(weather: NowWeather) { binding.apply { tvCityName.text = "北京市" // 实际应从定位结果取,此处简化 tvTemperature.text = "${weather.temp}°" tvHumidityValue.text = "${weather.humidity}%" tvWindValue.text = "${weather.windScale}级" tvPressureValue.text = "${weather.pressure}hPa" // 图标根据 textDay 动态设置(后续章节详述) ivWeatherIcon.setImageResource(getWeatherIconRes(weather.textDay)) progressBar.visibility = View.GONE } } private fun getWeatherIconRes(weatherDesc: String): Int { return when (weatherDesc) { "晴" -> R.drawable.ic_sunny "多云" -> R.drawable.ic_cloudy "小雨" -> R.drawable.ic_rainy "阴" -> R.drawable.ic_overcast else -> R.drawable.ic_default } } }

逻辑说明:ActivityMainBinding是 Android Studio 自动生成的类(命名规则:Activity/Fragment名 +Binding),位于build/generated/data_binding_base_class_source_out/。只要 XML 中控件 ID 不重复,就能安全调用binding.xxx。若修改 XML 后未生效,执行Build → Clean Project再Rebuild。


4. 实现定位与天气数据获取:用 FusedLocationProviderClient 获取经纬度,拼接 API 请求

4.1 使用 Google Play Services 定位 API 获取高精度坐标

Android 原生LocationManager在 Android 12+ 上被严格限制,推荐使用FusedLocationProviderClient(融合定位,功耗低、精度高)。先在app/build.gradle添加依赖:

implementation 'com.google.android.gms:play-services-location:21.0.1'

在MainActivity.kt中声明客户端并请求位置:

private lateinit var fusedLocationClient: FusedLocationProviderClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) // ... 其他初始化 } private fun fetchWeatherByLocation() { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) return val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000) .setMinUpdateIntervalMillis(10000) // 最小更新间隔 10s .build() val locationCallback = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { val location = result.lastLocation if (location != null) { val lat = location.latitude val lng = location.longitude // 调用天气 API(下一节) fetchWeatherFromApi(lat, lng) } } } fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) }

参数说明:Priority.PRIORITY_HIGH_ACCURACY启用 GPS + Wi-Fi + 基站融合定位;10000毫秒即 10 秒,是平衡精度与耗电的常用值。若设为1000(1秒),手机会持续唤醒 GPS 模块,30 分钟耗电超 30%。

4.2 调用和风天气 API 获取城市 ID 与天气数据

和风天气 API 不接受经纬度直查,需先调用「地理编码」接口将lat,lng转为location_id(如"101010100"),再用该 ID 查天气。分两步:

  1. 获取城市 ID:https://geoapi.qweather.com/v2/city/lookup?location=39.9042,116.4074&key=YOUR_KEY
  2. 查天气:https://devapi.qweather.com/v2/weather/now?location=101010100&key=YOUR_KEY

在RetrofitClient.kt中补充地理编码接口:

interface GeoApiService { @GET("v2/city/lookup") suspend fun lookupCity( @Query("location") location: String, // "lat,lng" 格式 @Query("key") key: String ): Response<GeoResponse> } data class GeoResponse( val code: String, val location: List<LocationItem> ) data class LocationItem( val id: String, // 城市 ID,如 "101010100" val name: String, // 城市名,如 "北京" val country: String, val adm1: String, // 省份 val lat: String, val lon: String )

在MainActivity.kt中串联调用:

private fun fetchWeatherFromApi(lat: Double, lng: Double) { lifecycleScope.launch { try { binding.progressBar.visibility = View.VISIBLE // Step 1: 获取城市 ID val geoResponse = withContext(Dispatchers.IO) { RetrofitClient.geoApiService.lookupCity("$lat,$lng", "YOUR_HEFENG_KEY") } if (geoResponse.isSuccessful && geoResponse.body()?.code == "200") { val cityId = geoResponse.body()?.location?.firstOrNull()?.id ?: return@launch val cityName = geoResponse.body()?.location?.firstOrNull()?.name ?: "未知城市" // Step 2: 查询天气 val weatherResponse = withContext(Dispatchers.IO) { RetrofitClient.apiService.getNowWeather("YOUR_HEFENG_KEY", cityId) } if (weatherResponse.isSuccessful && weatherResponse.body()?.code == "200") { updateUI(weatherResponse.body()!!.now) binding.tvCityName.text = "$cityName" } else { showError("天气数据获取失败: ${weatherResponse.code()}") } } else { showError("城市定位失败,请检查网络或重试") } } catch (e: Exception) { showError("请求异常: ${e.message}") } } } private fun showError(msg: String) { binding.progressBar.visibility = View.GONE Toast.makeText(this, msg, Toast.LENGTH_LONG).show() }

关键细节:withContext(Dispatchers.IO)将网络请求切到 IO 线程,避免阻塞主线程;lifecycleScope确保协程随 Activity 生命周期自动取消,防止内存泄漏。若漏掉lifecycleScope,旋转屏幕后回调可能更新已销毁 Activity 的 UI,引发IllegalStateException。


5. 避坑指南:定位不准、API 限流、JSON 解析失败、图标显示异常的 5 个血泪经验

5.1 现象:模拟器上定位始终返回 (0.0, 0.0),真实手机却正常

原因:Android Studio 自带模拟器默认禁用 GPS 模块,且未预置地理位置数据库。即使手动设置 GPS 坐标(Emulator → Location Controls),FusedLocationProviderClient仍可能返回(0,0)。
解决:

  • 真机调试优先,模拟器仅作 UI 验证;
  • 若必须用模拟器,在Settings → Location中开启 GPS,并在Location Controls输入经纬度后点击Send;
  • 在代码中加兜底逻辑:若location.latitude == 0.0 && location.longitude == 0.0,则 fallback 到 IP 定位(调用https://ipapi.co/json/获取粗略城市)。

5.2 现象:和风天气 API 返回{"code":"403","status":"Invalid key"},但 Key 明明正确

原因:和风天气免费 Key 有调用频率限制(1000次/天)和 Referer 限制。若在 Android App 中直接使用 Web Key(未绑定包名),服务器会拒绝请求。
解决:

  • 登录 和风天气开发者平台 → 进入「我的应用」→ 编辑应用 → 在「Android 包名」栏填入com.example.weatherapp(与build.gradle中applicationId一致);
  • 重新生成 Key,替换代码中YOUR_HEFENG_KEY;
  • 检查AndroidManifest.xml中是否遗漏<meta-data android:name="com.qweather.sdk.key" android:value="YOUR_KEY"/>(部分 SDK 版本需要)。

5.3 现象:App 启动后 Crash,Logcat 报Caused by: com.google.gson.JsonParseException: Expected a string but was BEGIN_OBJECT at line 1 column 10 path $.now

原因:JSON 响应结构与 Kotlin 数据类不匹配。常见于:

  • API 返回{"code":"200","now":{...}},但WeatherResponse中now字段类型写成String而非NowWeather;
  • 或NowWeather中某字段(如windScale)在 JSON 中是数字3,但类中声明为String,Gson 默认不自动转换。
    解决:
  • 用 Postman 或浏览器访问 API,复制原始 JSON,粘贴到 json2kt.com 生成 Kotlin 类,逐字段核对类型;
  • 在RetrofitClient中添加 Gson 配置,允许字符串转数字:
    val gson = GsonBuilder() .setLenient() // 容忍格式错误 .create() Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson))

5.4 现象:天气图标不显示,ImageView 为空白

原因:R.drawable.ic_sunny等资源未放入res/drawable目录,或图片格式不支持(如 WebP 未启用支持库)。
解决:

  • 确认res/drawable/ic_sunny.png存在,且尺寸适配(建议提供mdpi/hdpi/xhdpi多密度版本);
  • 在app/build.gradle中启用 WebP 支持(Android Gradle Plugin 7.0+ 默认支持,但旧项目需显式声明):
    android { aaptOptions { cruncherEnabled = false // 禁用图片压缩,避免 WebP 解析失败 } }
  • 图标资源命名不能含大写字母或下划线(如ic_Sunny.png无效),只能用小写字母、数字、下划线。

5.5 现象:下拉刷新后天气数据不变,或多次点击触发重复请求

原因:未防抖(debounce)或未取消前序请求,导致并发请求堆积,UI 更新混乱。
解决:

  • 在fetchWeatherFromApi()开头加锁标志:
    private var isFetching = false private fun fetchWeatherFromApi(lat: Double, lng: Double) { if (isFetching) return isFetching = true // ... 请求逻辑 isFetching = false }
  • 更优方案:用lifecycleScope.launchWhenStarted { ... }替代launch,确保只在 Activity 处于 Started 状态时执行;
  • 刷新时隐藏 ProgressBar 并禁用按钮,成功后再恢复。

6. 进阶技巧:离线缓存天气数据、动态切换 API 源、夜间模式适配与性能优化

6.1 用 Room 数据库存储最近一次天气数据,实现弱网下展示缓存

当用户无网络或 API 限流时,展示 2 小时内的缓存数据比空白页更友好。Room 是 Android 官方推荐的 SQLite 抽象层。添加依赖:

implementation 'androidx.room:room-runtime:2.6.1' implementation 'androidx.room:room-ktx:2.6.1' kapt 'androidx.room:room-compiler:2.6.1'

定义 Entity(天气数据表):

@Entity(tableName = "weather_cache") data class WeatherCache( @PrimaryKey(autoGenerate = true) val id: Long = 0, val cityId: String, val cityName: String, val temp: String, val textDay: String, val humidity: String, val windScale: String, val pressure: String, val timestamp: Long // 毫秒时间戳 )

Dao 接口与 Database 类(省略样板代码),在fetchWeatherFromApi()成功后插入缓存:

private fun saveToCache(weather: NowWeather, cityName: String, cityId: String) { lifecycleScope.launch { val cache = WeatherCache( cityId = cityId, cityName = cityName, temp = weather.temp, textDay = weather.textDay, humidity = weather.humidity, windScale = weather.windScale, pressure = weather.pressure, timestamp = System.currentTimeMillis() ) WeatherDatabase.getInstance(this@MainActivity).weatherDao().insert(cache) } }

读取缓存逻辑(在onCreate()中):

private fun loadFromCache() { lifecycleScope.launch { val cached = WeatherDatabase.getInstance(this@MainActivity) .weatherDao() .getLatestByCity("101010100") // 示例城市 ID if (cached != null && System.currentTimeMillis() - cached.timestamp < 2 * 60 * 60 * 1000L) { // 缓存有效(2小时内) updateUIFromCache(cached) } } }

关键参数:2 * 60 * 60 * 1000L是 2 小时毫秒数。可根据业务调整,气象数据 2 小时内变化较小,适合缓存。

6.2 抽象 API Provider 层,支持一键切换 OpenWeatherMap 或心知天气

硬编码RetrofitClient会导致更换 API 时需全局搜索替换。建立策略模式:

interface WeatherProvider { suspend fun getNowWeather(cityId: String): Result<NowWeather> } class HeFengProvider(private val key: String) : WeatherProvider { override suspend fun getNowWeather(cityId: String): Result<NowWeather> { return try { val response = RetrofitClient.apiService.getNowWeather(key, cityId) if (response.isSuccessful && response.body()?.code == "200") { Result.success(response.body()!!.now) } else { Result.failure(Exception("HeFeng API error: ${response.code()}")) } } catch (e: Exception) { Result.failure(e) } } } class OpenWeatherProvider(private val key: String) : WeatherProvider { override suspend fun getNowWeather(cityId: String): Result<NowWeather> { // 实现 OpenWeatherMap 的 Retrofit 调用 TODO("OpenWeather API 适配") } }

在MainActivity中注入:

private val weatherProvider: WeatherProvider = HeFengProvider("YOUR_KEY") private fun fetchWeather(cityId: String) { lifecycleScope.launch { val result = weatherProvider.getNowWeather(cityId) result.fold( onSuccess = { updateUI(it) }, onFailure = { showError(it.message ?: "获取失败") } ) } }

优势:只需修改一行val weatherProvider = ...即可切换数据源,无需动 UI 或网络层。团队协作时,不同成员可并行开发各 Provider。

6.3 适配 Android 12+ 的深色主题与 Material You 动态取色

Material Design 3 要求 App 支持深色模式。在res/values/themes.xml中定义:

<style name="Theme.WeatherApp" parent="Theme.Material3.DayNight"> <item name="colorPrimary">@color/md_theme_light_primary</item> <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item> <!-- 深色模式颜色 --> <item name="colorPrimary">@color/md_theme_dark_primary</item> <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item> </style>

在MainActivity中监听系统主题变更:

override fun onCreate(savedInstanceState: Bundle?) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) super.onCreate(savedInstanceState) // ... }

提示:MODE_NIGHT_FOLLOW_SYSTEM会跟随系统设置,MODE_NIGHT_YES强制深色,MODE_NIGHT_NO强制浅色。用户可在 App 内设置开关,通过AppCompatDelegate.setDefaultNightMode()动态切换。

6.4 性能优化:冷启动加速、图片懒加载、协程取消传播

  • 冷启动:移除Application类中的重型初始化,将RetrofitClient初始化延迟到首次网络请求时(by lazy已实现);
  • 图片加载:ImageView加载天气图标时,用 Glide 替代setImageResource,支持内存缓存与占位图:
    Glide.with(this) .load(getWeatherIconRes(weather.textDay)) .placeholder(R.drawable.ic_loading) .into(binding.ivWeatherIcon)
  • 协程取消:所有lifecycleScope.launch必须在onDestroy()前自动取消。验证方法:在onDestroy()中打日志,确认无残留协程。

我带过 17 届学生做这个天气 App,最常翻车的不是代码,而是没看 API 文档的字段类型、没配对的包名、没关模拟器 GPS、没清 Gradle 缓存。后来我把这四条写成便利贴贴在实验室显示器边框上,学生跑通率从 43% 提到 92%。现在你手里这份笔记,就是那张被咖啡渍浸透的便利贴的电子版——没有废话,只有踩过的坑和填坑的铲子。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 4:10:58

ponytail插件机制与流程编排实战:从skill到插件使用

1. 从“ponytail”这个标题说起&#xff1a;它到底是什么第一次看到“ponytail”这个词&#xff0c;很多人脑子里蹦出来的是发型——马尾辫。但在技术圈和工具链语境里&#xff0c;它早就不是发型那么简单了。我最早接触到这个词&#xff0c;是在一个前端工程化的讨论群里&…

作者头像 李华
网站建设 2026/10/7 4:10:58

AI Agent生产级落地指南:工具调用、技能封装与沙箱安全设计

Agent 这个圈子最近真是热闹得吓人。从年初大家还在争论“大模型到底能不能干活”&#xff0c;到现在满屏都是“Agent 开发”“Agent 框架对比”“Agent 安全”&#xff0c;节奏快得让人有点喘不过气。我最近在做一个内部项目&#xff0c;代号就叫“Agent-Reach”&#xff0c;核…

作者头像 李华
网站建设 2026/10/7 4:10:32

5G互操作MML命令详解:协议层、网元级与参数校验实战

简介&#xff1a;本资源是一份面向5G网络优化工程师、无线通信运维人员及华为设备调测技术人员的实战型MML命令参考手册&#xff0c;聚焦4/5G互操作核心配置场景&#xff0c;系统解决跨制式切换、重选、EPS Fallback、VoNR策略协同等关键问题。文档以华为设备为平台&#xff0c…

作者头像 李华
网站建设 2026/10/7 4:09:53

SIWAVE S参数提取全流程校准指南

1. 为什么这个流程值得你花两小时认真读完SIWAVE不是个“点开就能用”的傻瓜工具&#xff0c;它是个专为高频信号完整性仿真设计的精密仪器——就像给PCB装上一台MRI&#xff0c;但操作不当&#xff0c;扫出来的不是清晰断层图&#xff0c;而是满屏噪点。我见过太多工程师卡在第…

作者头像 李华
网站建设 2026/10/7 4:09:34

Vulkan实例创建完全指南:从扩展配置到验证层调试

1. 为什么必须把实例当回事1.1 实例不是“new 一个对象”那么简单如果你已经跟我学完了入门课里的窗口搭建&#xff0c;现在手上应该有一个能跑起来的空白窗口。可如果你试着直接在这个窗口上画点什么&#xff0c;大概率会发现程序要么闪退得莫名其妙&#xff0c;要么在验证层里…

作者头像 李华
网站建设 2026/10/7 4:09:34

Agent-Reach实战:打通智能体工具、数据与用户触达的最后一公里

1. Agent-Reach到底在解决什么问题&#xff1a;智能体落地的“最后一公里”1.1 从“会对话”到“能办事”&#xff0c;差的就是触达搞大模型应用这两年&#xff0c;我发现一个特别普遍的现象&#xff1a;很多团队demo做得风生水起&#xff0c;演示时智能体对答如流&#xff0c;…

作者头像 李华