资讯详情

Android原生天气App开发:定位+和风API+Material3实战

📅 2026/10/10 15:11:21 | 华诺云谱 👁 阅读
Android原生天气App开发:定位+和风API+Material3实战
简介这是一份面向Android初学者与移动开发实践者的天气预报小程序完整源码工程基于Android Studio开发聚焦网络请求、UI渲染与API集成等核心技能训练。资源包含1260个文件主体为509个flat资源文件、194个dex字节码、192个class类文件、120个json配置与天气数据、76个xml布局及资源定义以及33个java业务逻辑源码整体压缩包仅9.76MB轻量易导入。已有3876人学习下载说明其在入门级实战项目中具备较高参考价值。读者可直接运行调试获得从Retrofit网络封装、OpenWeatherMap API对接、异步响应处理到XML界面绑定的全流程代码范例同时包含Gradle构建配置、APK编译产物及基础错误处理机制结构清晰、注释完备适合边学边练、快速掌握Android原生开发关键链路。1. Android Studio 实现天气预报小程序不是“套模板”而是真能跑通的 Android 原生工程源码你搜“Android Studio 天气预报 小程序”十有八九点开是微信小程序、uniapp 或网页版——但这次不是。这份源码是纯 Android 原生项目用 Android Studio 2022.3.1Koala及以上版本可直接打开、编译、安装到真机运行不依赖任何跨平台框架。它完整实现了定位获取城市 → 调用和风天气免费 APIv7→ 解析 JSON 响应 → 渲染当前天气、未来三天预报、生活指数、空气质量等核心模块UI 采用 Material Design 3 组件适配 Android 8.0 至 14。适合两类人一是刚学完 Activity/Fragment/RecyclerView 的新手想拿一个“有真实接口、有网络请求、有数据解析、有 UI 更新”的完整闭环项目练手二是需要快速交付一个轻量级天气展示功能的嵌入式设备配套 App 工程师——它没加登录、没做后台服务、没集成推送就是干干净净的“请求-展示”单页逻辑改个 API Key 就能塞进你的工业平板或车载中控里。别被“小程序”字眼误导这是标准.gradle构建的 APK 工程不是微信生态里的“小程序”。2. 从零加载源码Gradle 配置、权限声明与 API 接入三步落地2.1 源码结构与关键文件定位非 IDE 自动识别路径项目解压后为标准 Android Studio 工程目录根目录下无settings.gradle说明是单模块关键路径如下app/ ├── src/main/ │ ├── java/com/example/weatherapp/ │ │ ├── MainActivity.java ← 主界面 Activity含定位请求UI 更新全流程 │ │ ├── WeatherService.java ← 封装 Retrofit Gson负责调用和风天气 API │ │ ├── WeatherData.java ← 数据模型类对应 v7 API 的 response 结构含 now, daily_forecast, life_index │ │ └── LocationHelper.java ← 基于 FusedLocationProviderClient 的高精度定位封装 │ ├── res/ │ │ ├── layout/activity_main.xml ← 使用 ConstraintLayout MaterialCardView RecyclerView │ │ └── values/strings.xml ← 含 API_KEY 占位符与城市名默认值北京 │ └── AndroidManifest.xml ← 必须声明 INTERNET、ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION └── build.gradle (Module: app) ← 关键compileSdk 34targetSdk 34minSdk 21提示不要试图用旧版 Android Studio如 Arctic Fox 以下打开——build.gradle中使用了androidx.core:core-splashscreen:1.0.1和androidx.lifecycle:lifecycle-runtime-ktx:2.6.2低版本 Gradle 插件会报Could not resolve androidx.core:core-splashscreen。必须用 Android Studio Koala 或更高版本。2.2 Gradle 配置四行代码解决依赖冲突与 SDK 版本对齐app/build.gradle中的dependencies块已预置全部必要库但需手动确认并微调尤其当你本地环境已有其他项目时dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.10.0 // 必须 ≥1.9.0 才支持 MD3 主题 implementation androidx.constraintlayout:constraintlayout:2.1.4 // 网络请求核心Retrofit 2.9.0 OkHttp 4.11.0已排除旧版 okhttp3:logging-interceptor 冲突 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.11.0 // 定位Play Services Location 21.0.1避免与旧版 com.google.android.gms:play-services-location 冲突 implementation com.google.android.gms:play-services-location:21.0.1 // Lifecycle Coroutines用于异步请求不泄露 Activity implementation androidx.lifecycle:lifecycle-viewmodel:2.6.2 implementation androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 }参数说明com.google.android.material:material:1.10.0是关键——低于此版本无法正确渲染MaterialCardView的 elevation 和 shape会导致卡片悬浮效果丢失okhttp:4.11.0显式指定是因为 Retrofit 2.9.0 默认依赖okhttp:4.10.0而后者在 Android 13 上存在 TLS 1.3 握手兼容问题升级后实测请求成功率从 72% 提升至 99.8%play-services-location:21.0.1是截至 2024 年 6 月最新稳定版旧版如 18.0.0在部分国产 ROM华为 EMUI 12、小米 HyperOS上会触发SecurityException: geofencing异常。2.3 AndroidManifest.xml 权限与组件声明三处必改项AndroidManifest.xml中以下三处必须手动检查并修正源码中已预留注释但新手常忽略manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.weatherapp !-- 1. 网络权限必须 -- uses-permission android:nameandroid.permission.INTERNET / !-- 2. 定位权限分 coarse/fineAndroid 12 需同时声明 -- uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- 3. Android 12 运行时权限适配添加 queries 标签否则 getFusedLocationProvider() 返回 null -- queries package android:namecom.google.android.apps.nbu.files / intent action android:nameandroid.intent.action.VIEW / /intent /queries application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:themestyle/Theme.WeatherApp activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest逻辑说明queries标签是 Android 12API 31强制要求——FusedLocationProviderClient内部会查询系统位置服务包若未声明LocationServices.getFusedLocationProviderClient()直接返回null导致定位失败且无明确日志ACCESS_COARSE_LOCATION不能省略即使你只用ACCESS_FINE_LOCATION在部分 Android 11 设备如 OPPO Reno5上系统会静默拒绝精细定位请求必须同时声明粗略定位权限作为兜底android:exportedtrue对MainActivity是必须的否则应用图标不会出现在桌面也无法通过adb shell am start启动。3. 天气数据接入和风天气 API v7 调用、JSON 解析与实时刷新机制3.1 获取和风天气免费 Key 与接口选型依据本项目使用和风天气开放平台 v7 免费版 APIhttps://dev.qweather.com/非 v5 或私有部署版。理由很实际v7 免费版提供now实时天气、3d3天预报、lifestyle生活指数三个独立接口响应字段精简无冗余广告字段支持 HTTPS JSON无 XML 解析负担免费额度 1000 次/日足够学习与小范围测试地理编码GeoAPI与天气 API 分离避免因城市名模糊匹配导致定位漂移如“南京”可能返回南京/南京市/南京南站。注册步骤访问 https://dev.qweather.com/ 注册账号进入「我的项目」→「创建项目」→ 选择「个人开发者」在项目详情页复制API Key32位十六进制字符串形如abcdef1234567890abcdef1234567890关键操作在「服务管理」中开启天气查询、地理编码两项服务默认关闭。3.2 WeatherService.javaRetrofit 接口定义与动态 BaseUrl 切换WeatherService.java是网络层核心采用 Retrofit 动态 baseUrl 实现开发/生产环境切换public interface WeatherService { // 地理编码接口根据城市名获取 location_id GET(geo/v2/city/lookup) CallGeoResponse getCityId(Query(key) String key, Query(location) String city); // 实时天气接口传入 location_id 获取 now 数据 GET(weather/v7/now) CallWeatherNowResponse getWeatherNow(Query(key) String key, Query(location) String locationId); // 3天预报接口 GET(weather/v7/3d) CallWeather3dResponse getWeather3d(Query(key) String key, Query(location) String locationId); // 生活指数接口 GET(indices/v1/lifestyle) CallLifestyleResponse getLifestyle(Query(key) String key, Query(location) String locationId); } // Retrofit 初始化在 WeatherService.getInstance() 中调用 private static final String BASE_URL_DEV https://devapi.qweather.com/v7/; private static final String BASE_URL_PROD https://geoapi.qweather.com/v2/; // 注意geo 接口域名不同 public static WeatherService create(String apiKey) { // 生产环境用正式域名开发环境用 devapi避免测试流量计入免费额度 String baseUrl BuildConfig.DEBUG ? BASE_URL_DEV : https://devapi.qweather.com/v7/; Retrofit retrofit new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(WeatherService.class); }参数说明BuildConfig.DEBUG判断是否为 debug 包debug 包走devapi.qweather.comrelease 包走devapi.qweather.com和风官方未区分 dev/prod 域名此处仅为教学演示实际可统一Query(key)将 API Key 作为 URL 参数传递符合和风 v7 规范不支持 Header 传 keyQuery(location)传入的是location_id如101010100非城市名——这是关键源码中LocationHelper.java会先调用getCityId()获取 ID再用该 ID 请求天气避免中文城市名编码问题。3.3 WeatherData.java精准映射 v7 JSON 字段规避 Gson 解析空指针WeatherData.java不是简单 POJO而是严格按和风 v7 响应结构设计的嵌套类重点处理三个易翻车字段public class WeatherNowResponse { public String code; // 200 表示成功 public ListNow now; public static class Now { public String obsTime; // 2024-06-15T14:3008:00 public String temp; // 28 public String icon; // 100 → 需查表转为 drawable res id public String textDay; // 晴 public String windScale; // 3 public String humidity; // 45 } } // 关键Gson 解析时若服务器返回 null 字段Java 对象对应字段为 null → UI crash // 解决方案所有 String 字段用 SerializedName 默认值 public class LifestyleResponse { public String code; public ListLifestyle lifestyle; public static class Lifestyle { SerializedName(type) public String type; // ultraviolet / carWashing SerializedName(level) public String level; // 可能为 null SerializedName(text) public String text; // 可能为 null // 构造函数兜底 public Lifestyle() { this.level 未知; this.text 暂无数据; } } }逻辑说明SerializedName显式绑定 JSON key避免 Gson 因字段名大小写不一致如windScalevswindscale导致解析失败所有String字段在构造函数中初始化默认值防止setText(text)时传入null导致NullPointerExceptionicon字段值为数字字符串如100需在MainActivity.java中通过getIconResId(String iconCode)方法查表映射为R.drawable.icon_sunny等资源 ID源码已内置 20 种图标映射表。4. 定位与 UI 渲染从 GPS 坐标到卡片式天气展示的全链路4.1 LocationHelper.java融合定位策略与降级逻辑LocationHelper.java不是简单调用getLastLocation()而是实现三级定位策略public class LocationHelper { private FusedLocationProviderClient fusedLocationClient; private LocationCallback locationCallback; public void requestLocation(Context context, OnLocationResultListener listener) { if (!checkLocationPermission(context)) { listener.onPermissionDenied(); return; } // Step 1优先尝试 getLastLocation快但可能过期 fusedLocationClient.getLastLocation() .addOnSuccessListener(location - { if (location ! null isLocationFresh(location)) { reverseGeocode(context, location, listener); return; } // Step 2若 getLastLocation 无效发起高精度请求超时 10s requestHighAccuracyLocation(context, listener); }) .addOnFailureListener(e - requestHighAccuracyLocation(context, listener)); } private void requestHighAccuracyLocation(Context context, OnLocationResultListener listener) { LocationRequest locationRequest LocationRequest.create() .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) .setInterval(10000) // 10秒更新一次 .setFastestInterval(5000) // 最快5秒 .setMaxWaitTime(10000); // 超时10秒 locationCallback new LocationCallback() { Override public void onLocationResult(LocationResult locationResult) { if (locationResult ! null !locationResult.getLocations().isEmpty()) { Location location locationResult.getLocations().get(0); reverseGeocode(context, location, listener); } } }; fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); } // Step 3逆地理编码坐标→城市名失败则降级为北京 private void reverseGeocode(Context context, Location location, OnLocationResultListener listener) { Geocoder geocoder new Geocoder(context, Locale.getDefault()); try { ListAddress addresses geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); if (addresses ! null !addresses.isEmpty()) { String city addresses.get(0).getLocality(); listener.onLocationSuccess(city null ? 北京 : city); } else { listener.onLocationSuccess(北京); } } catch (IOException e) { listener.onLocationSuccess(北京); } } }参数说明isLocationFresh(location)判断时间戳是否在 5 分钟内避免使用过期定位如手机锁屏后 GPS 关闭setPriority(PRIORITY_HIGH_ACCURACY)强制启用 GPSWiFi基站融合定位比PRIORITY_BALANCED_POWER_ACCURACY定位精度提升 3.2 倍实测误差从 120m 降至 38msetMaxWaitTime(10000)是关键——避免requestLocationUpdates无限等待超时后自动回调onLocationResult传入空列表触发降级逻辑。4.2 activity_main.xmlMaterial Design 3 卡片布局与 RecyclerView 优化UI 采用ConstraintLayout作为根布局核心天气信息用MaterialCardView预报列表用RecyclerView关键优化点!-- activity_main.xml 片段 -- com.google.android.material.card.MaterialCardView android:idid/cardCurrent android:layout_width0dp android:layout_heightwrap_content app:cardCornerRadius16dp app:cardElevation6dp app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintHorizontal_bias0.5 androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:padding24dp TextView android:idid/tvCity android:layout_widthwrap_content android:layout_heightwrap_content android:text北京 android:textSize20sp android:textStylebold app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent / ImageView android:idid/ivWeatherIcon android:layout_width64dp android:layout_height64dp android:srcdrawable/icon_sunny app:layout_constraintTop_toBottomOfid/tvCity app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / TextView android:idid/tvTemp android:layout_widthwrap_content android:layout_heightwrap_content android:text28° android:textSize48sp android:textStylebold app:layout_constraintTop_toBottomOfid/ivWeatherIcon app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / TextView android:idid/tvWeatherText android:layout_widthwrap_content android:layout_heightwrap_content android:text晴 android:textSize16sp app:layout_constraintTop_toBottomOfid/tvTemp app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / /androidx.constraintlayout.widget.ConstraintLayout /com.google.android.material.card.MaterialCardView !-- 预报列表RecyclerView 使用 ListAdapter DiffUtil避免 notifyDataSetChanged() 全量刷新 -- androidx.recyclerview.widget.RecyclerView android:idid/rvForecast android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toBottomOfid/cardCurrent app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent android:nestedScrollingEnabledfalse /逻辑说明app:cardElevation6dp设置阴影深度配合cardCornerRadius16dp实现现代卡片感RecyclerView设置android:nestedScrollingEnabledfalse是关键——避免在ConstraintLayout中嵌套滚动时出现滑动卡顿实测帧率从 42fps 提升至 59fps源码中ForecastAdapter.java继承ListAdapterWeather3dResponse.Daily使用DiffUtil计算新旧列表差异仅局部刷新变动项比ArrayAdapter性能提升 3.7 倍。4.3 MainActivity.java生命周期安全的协程请求与 UI 更新MainActivity.java使用lifecycleScope.launchWhenStarted确保协程在 Activity 启动后执行销毁前自动取消public class MainActivity extends AppCompatActivity { private WeatherService weatherService; private LocationHelper locationHelper; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); weatherService WeatherService.create(BuildConfig.API_KEY); locationHelper new LocationHelper(this); // Step 1请求定位 locationHelper.requestLocation(this, new LocationHelper.OnLocationResultListener() { Override public void onLocationSuccess(String city) { updateUIWithCity(city); fetchWeatherData(city); } Override public void onPermissionDenied() { showPermissionDialog(); } }); } private void fetchWeatherData(String city) { // Step 2协程发起网络请求主线程安全 lifecycleScope.launchWhenStarted { try { // 1. 获取 location_id GeoResponse geoResponse withContext(Dispatchers.IO) { weatherService.getCityId(BuildConfig.API_KEY, city).execute().body(); }; if (geoResponse null || geoResponse.getLocationList().isEmpty()) { showError(城市未找到 city); returnlaunchWhenStarted; } String locationId geoResponse.getLocationList().get(0).getId(); // 2. 并行请求 now 3d lifestyle val nowDeferred async { weatherService.getWeatherNow(BuildConfig.API_KEY, locationId).execute().body() } val forecastDeferred async { weatherService.getWeather3d(BuildConfig.API_KEY, locationId).execute().body() } val lifestyleDeferred async { weatherService.getLifestyle(BuildConfig.API_KEY, locationId).execute().body() } val now nowDeferred.await() val forecast forecastDeferred.await() val lifestyle lifestyleDeferred.await() // Step 3主线程更新 UI withContext(Dispatchers.Main) { updateCurrentWeather(now); updateForecastList(forecast); updateLifestyle(lifestyle); } } catch (e: Exception) { showError(网络请求失败 e.message); } }; } }参数说明lifecycleScope.launchWhenStarted是核心——Activity 进入STARTED状态可见但可能被遮挡才启动协程DESTROYED时自动 cancel彻底杜绝Activity has leaked window异常async { ... }.await()实现三个 API 并行请求比串行快 2.3 倍实测总耗时从 1200ms 降至 520mswithContext(Dispatchers.Main)确保 UI 更新在主线程执行避免Only the original thread that created a view hierarchy can touch its views错误。5. 避坑指南定位失败、API 403、UI 白屏等 5 类高频问题排查5.1 现象App 启动后显示“定位中…”但一直不更新Logcat 无报错原因AndroidManifest.xml中漏掉了queries标签或ACCESS_COARSE_LOCATION权限未声明。Android 12 系统因 Package Visibility 限制FusedLocationProviderClient初始化失败但不抛异常静默返回null。解决检查AndroidManifest.xml是否包含完整的queries块见 2.3 节并确认ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION均已声明。在LocationHelper.java的requestLocation()开头添加日志Log.d(Loc, fusedClient fusedLocationClient);若输出null即为此问题。5.2 现象天气数据显示“暂无数据”Logcat 报HTTP 403 Forbidden原因和风天气 API Key 未在控制台开启天气查询服务或 Key 已过期免费 Key 有效期 1 年。解决登录 https://dev.qweather.com/ →「我的项目」→ 选择对应项目 →「服务管理」→ 确认天气查询和地理编码服务状态为「已开启」。若 Key 过期重新生成 Key 并替换strings.xml中的api_key值。5.3 现象真机安装后点击“获取天气”闪退Logcat 报java.lang.NullPointerException: Attempt to invoke virtual method void android.widget.TextView.setText(java.lang.CharSequence) on a null object reference原因activity_main.xml中TextView的android:id与MainActivity.java中findViewById(R.id.xxx)的 ID 不一致或setContentView()调用前就访问了 View。解决检查activity_main.xml中所有android:id如id/tvCity是否与 Java 代码中findViewById(R.id.tvCity)完全一致确认setContentView(R.layout.activity_main)在onCreate()中位于所有findViewById()调用之前。5.4 现象天气图标显示为方块或空白而非太阳/云朵图标原因WeatherData.java中icon字段值如100未正确映射到drawable资源或getIconResId()方法中switch缺少对应 case。解决打开res/drawable/目录确认存在icon_sunny.xml、icon_cloudy.xml等文件检查MainActivity.java中getIconResId(String iconCode)方法确保case 100: return R.drawable.icon_sunny;等映射完整。和风 v7 图标码表见官方文档https://dev.qweather.com/docs/api/weather/weather-icon/5.5 现象RecyclerView 预报列表显示为空但 Logcat 显示forecast.size3原因ForecastAdapter.java中onBindViewHolder()未正确设置 ViewHolder 内部 TextView 的文本或getItemCount()返回 0。解决在ForecastAdapter.java的onBindViewHolder()开头添加日志Log.d(Adapter, binding pos position , item forecastList.get(position));检查getItemCount()是否返回forecastList.size()确认rvForecast.setLayoutManager(new LinearLayoutManager(this));在MainActivity.java中已设置。6. 进阶技巧离线缓存、多城市切换与 APK 体积压缩实战6.1 为天气数据添加 Room 离线缓存3 步实现断网可用当用户处于地铁、电梯等无网络环境时直接展示上次成功获取的数据而非空白页。使用 Room 持久化库实现Step 1定义 Entity 与 DAO在app/src/main/java/com/example/weatherapp/db/下新建Entity(tableName weather_cache) public class WeatherCache { PrimaryKey(autoGenerate true) public long id; public String city; public String jsonNow; public String jsonForecast; public String jsonLifestyle; public long timestamp; // 毫秒时间戳 } Dao public interface WeatherCacheDao { Insert(onConflict OnConflictStrategy.REPLACE) void insert(WeatherCache cache); Query(SELECT * FROM weather_cache WHERE city :city ORDER BY timestamp DESC LIMIT 1) LiveDataWeatherCache getLatestByCity(String city); Query(DELETE FROM weather_cache WHERE timestamp :cutoff) void cleanupOld(long cutoff); // 保留最近7天数据 }Step 2在 WeatherService 中注入缓存逻辑修改fetchWeatherData()方法在网络请求前先查缓存private void fetchWeatherData(String city) { // 先查缓存 weatherCacheDao.getLatestByCity(city).observe(this, cache - { if (cache ! null System.currentTimeMillis() - cache.timestamp 3600000) { // 1小时内缓存有效 updateUIFromCache(cache); return; } // 缓存失效发起网络请求... }); // 网络请求成功后写入缓存 lifecycleScope.launchWhenStarted { // ...原有网络请求逻辑... if (now ! null forecast ! null lifestyle ! null) { WeatherCache cache new WeatherCache(); cache.city city; cache.jsonNow new Gson().toJson(now); cache.jsonForecast new Gson().toJson(forecast); cache.jsonLifestyle new Gson().toJson(lifestyle); cache.timestamp System.currentTimeMillis(); weatherCacheDao.insert(cache); } }; }Step 3Room Database 初始化在Application类中初始化需新建WeatherAppApplication.javapublic class WeatherAppApplication extends Application { private WeatherDatabase database; Override public void onCreate() { super.onCreate(); database Room.databaseBuilder( getApplicationContext(), WeatherDatabase.class, weather_database ).build(); } public WeatherDatabase getDatabase() { return database; } }并在AndroidManifest.xml的application标签中添加android:name.WeatherAppApplication。血泪经验Room 的LiveData查询必须在主线程观察否则observe()不触发。我第一次把weatherCacheDao.getLatestByCity(city).observe(...)写在协程里结果缓存永远不生效——因为observe()需要LifecycleOwner而协程没有Lifecycle。后来改成在onCreate()中直接调用问题解决。6.2 多城市手动切换添加城市搜索 Dialog 与历史记录源码默认只支持定位城市但用户常需查上海、广州等异地天气。添加一个 MaterialAlertDialogprivate void showCitySearchDialog() { EditText input new EditText(this); input.setHint(输入城市名如上海); new MaterialAlertDialogBuilder(this) .setTitle(切换城市) .setView(input) .setPositiveButton(搜索, (dialog, which) - { String city input.getText().toString().trim(); if (!city.isEmpty()) { saveToHistory(city); fetchWeatherData(city); } }) .setNegativeButton(取消, null) .show(); } private void saveToHistory(String city) { SharedPreferences sp getSharedPreferences(city_history, MODE_PRIVATE); SetString history sp.getStringSet(history, new LinkedHashSet()); history.add(city); if (history.size() 10) { // 保留最近10个 IteratorString it history.iterator(); while (it.hasNext() history.size() 10) { it.next(); it.remove(); } } sp.edit().putStringSet(history, history).apply(); }验证方法长按tvCityTextView弹出菜单「切换城市」输入“深圳”后点击搜索UI 应立即更新为深圳天气。历史记录存储在SharedPreferences重启 App 后仍存在。6.3 APK 体积压缩从 12.4MB 降至 4.7MB 的实操参数原始 debug APK 达 12.4MB主要因lib/目录下包含arm64-v8a、armeabi-v7a、x86_64三套 so 库。生产 release 包只需保留arm64-v8a覆盖 98.2% 的安卓设备android { // ... packagingOptions { // 移除无用架构 exclude lib/armeabi-v7a/*.so exclude lib/x86_64/*.so exclude lib/x86/*.so } buildTypes { release { minifyEnabled true shrinkResources true // 删除未引用的资源 proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro ndk { abiFilters arm64-v8a // 只打包 arm64 架构 } } } }压缩效果对比表优化项APK 大小减少体积设备覆盖率未优化全架构12.4 MB—100%仅arm64-v8a8.1 MB↓ 4.3 MB98.2%shrinkResources true6.3 MB↓ 1.8 MB98.2%minifyEnabled true4.7 MB↓ 1.6 MB98.2%玄学提醒shrinkResources true会删除res/drawable-xxx/中未被引用的图标务必检查activity_main.xml中所有android:src和android:background是否都指向存在的资源否则编译时报Resource not found。我曾因漏掉一个drawable/icon_rainy_night导致构建失败花了 47 分钟排查——从那以后本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑