ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 Giraffe2022.3.1及 AGP 8.1.0 验证适配 Android 10API 29至 Android 14API 34无 Gradle 插件冲突无模拟器兼容性玄学问题。2. 从零搭建工程结构创建空项目、配置网络与定位权限、引入 Retrofit Gson2.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.gradlemodule-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:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / !-- Android 12 需额外声明 foreground service 权限仅当需后台持续定位 -- uses-permission android:nameandroid.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: Arrayout 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.ktinterface WeatherApiService { GET(v2/weather/now) suspend fun getNowWeather( Query(key) key: String, Query(location) location: String ): ResponseWeatherResponse GET(v2/weather/3d) suspend fun get3DayForecast( Query(key) key: String, Query(location) location: String ): ResponseForecastResponse } // 单例 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:2208:00, temp: 26, feelsLike: 28, textDay: 多云, wind360: 90, windDir: 东风, windScale: 3, humidity: 65, precip: 0.0, pressure: 1005 } }对应创建data/WeatherResponse.ktdata 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。若误写成IntGson 解析时会抛JsonParseExceptionApp 直接 Crash。同理humidity是65不是65。这是免费 API 的典型设计务必按实际 JSON 类型建模。3.2 编写 activity_main.xml 布局文件Material Design 风格使用ConstraintLayout实现响应式布局重点元素顶部城市名 TextView、中央大温度 Textview、天气图标 ImageView、下方参数网格湿度/风力/气压。关键点所有控件 ID 必须唯一且与 ViewBinding 生成类字段名一致。?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools android:layout_widthmatch_parent android:layout_heightmatch_parent tools:context.MainActivity TextView android:idid/tvCityName android:layout_widthwrap_content android:layout_heightwrap_content android:text北京市 android:textSize24sp android:textStylebold app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent android:layout_marginTop32dp / TextView android:idid/tvTemperature android:layout_widthwrap_content android:layout_heightwrap_content android:text26° android:textSize72sp android:textStylebold app:layout_constraintTop_toBottomOfid/tvCityName app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent android:layout_marginTop24dp / ImageView android:idid/ivWeatherIcon android:layout_width80dp android:layout_height80dp android:srcdrawable/ic_cloudy app:layout_constraintTop_toBottomOfid/tvTemperature app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent android:layout_marginTop16dp / !-- 参数网格3列2行 -- androidx.gridlayout.widget.GridLayout android:idid/gridParams android:layout_widthmatch_parent android:layout_heightwrap_content app:layout_constraintTop_toBottomOfid/ivWeatherIcon app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent android:layout_marginTop32dp app:columnCount3 app:rowCount2 TextView android:idid/tvHumidityLabel android:layout_width0dp android:layout_heightwrap_content android:text湿度 android:gravitycenter app:layout_columnWeight1 / TextView android:idid/tvWindLabel android:layout_width0dp android:layout_heightwrap_content android:text风力 android:gravitycenter app:layout_columnWeight1 / TextView android:idid/tvPressureLabel android:layout_width0dp android:layout_heightwrap_content android:text气压 android:gravitycenter app:layout_columnWeight1 / TextView android:idid/tvHumidityValue android:layout_width0dp android:layout_heightwrap_content android:text65% android:gravitycenter app:layout_columnWeight1 / TextView android:idid/tvWindValue android:layout_width0dp android:layout_heightwrap_content android:text3级 android:gravitycenter app:layout_columnWeight1 / TextView android:idid/tvPressureValue android:layout_width0dp android:layout_heightwrap_content android:text1005hPa android:gravitycenter app:layout_columnWeight1 / /androidx.gridlayout.widget.GridLayout ProgressBar android:idid/progressBar android:layout_widthwrap_content android:layout_heightwrap_content app:layout_constraintTop_toBottomOfid/gridParams app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent android:layout_marginTop32dp android:visibilitygone / /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 秒是平衡精度与耗电的常用值。若设为10001秒手机会持续唤醒 GPS 模块30 分钟耗电超 30%。4.2 调用和风天气 API 获取城市 ID 与天气数据和风天气 API 不接受经纬度直查需先调用「地理编码」接口将lat,lng转为location_id如101010100再用该 ID 查天气。分两步获取城市 IDhttps://geoapi.qweather.com/v2/city/lookup?location39.9042,116.4074keyYOUR_KEY查天气https://devapi.qweather.com/v2/weather/now?location101010100keyYOUR_KEY在RetrofitClient.kt中补充地理编码接口interface GeoApiService { GET(v2/city/lookup) suspend fun lookupCity( Query(location) location: String, // lat,lng 格式 Query(key) key: String ): ResponseGeoResponse } data class GeoResponse( val code: String, val location: ListLocationItem ) 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 ?: returnlaunch 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 ControlsFusedLocationProviderClient仍可能返回(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:namecom.qweather.sdk.key android:valueYOUR_KEY/部分 SDK 版本需要。5.3 现象App 启动后 CrashLogcat 报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但类中声明为StringGson 默认不自动转换。解决用 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(thisMainActivity).weatherDao().insert(cache) } }读取缓存逻辑在onCreate()中private fun loadFromCache() { lifecycleScope.launch { val cached WeatherDatabase.getInstance(thisMainActivity) .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): ResultNowWeather } class HeFengProvider(private val key: String) : WeatherProvider { override suspend fun getNowWeather(cityId: String): ResultNowWeather { 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): ResultNowWeather { // 实现 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 nameTheme.WeatherApp parentTheme.Material3.DayNight item namecolorPrimarycolor/md_theme_light_primary/item item namecolorOnPrimarycolor/md_theme_light_onPrimary/item !-- 深色模式颜色 -- item namecolorPrimarycolor/md_theme_dark_primary/item item namecolorOnPrimarycolor/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%。现在你手里这份笔记就是那张被咖啡渍浸透的便利贴的电子版——没有废话只有踩过的坑和填坑的铲子。希望帮到你。本文还有配套的精品资源点击获取
返回列表