尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android天气APP高分毕设工程实践指南

Android天气APP高分毕设工程实践指南 简介本资源是一套高分通过的Android本科毕业设计项目源码面向计算机及相关专业本科生、移动开发初学者及课程设计实践者提供完整可运行的天气预报APP系统实现方案。项目聚焦Android原生开发核心能力训练涵盖UI界面构建、网络请求集成、天气数据解析与本地缓存等典型模块适用于毕业设计、期末大作业及Android进阶实训场景。压缩包共666个文件含108个Java业务逻辑文件、188个XML布局与配置资源、167个编译生成的Class字节码以及98张PNG图标、21个Jar依赖库和12个JSON数据模板整体大小为24.26MB结构规范模块划分清晰。已有194人下载学习源码经严格调试评审得分高达97分附带SlidingMenu、ViewPagerIndicator等主流第三方库AAR组件及MediaSessionCompat等系统级AIDL接口定义便于理解Android组件通信与架构整合实践。1. 这不是“做个天气APP交差”——它是一套可落地、可答辩、可延展的 Android 工程实践闭环很多本科生拿到“基于Android的天气预报APP”这个毕设题目时第一反应是网上随便下个源码改改UI、换换API Key就能过。但现实是高分项目从来不是拼“有没有”而是拼“为什么这么设计”“哪里可能出错”“怎么证明它可靠”。真正拉开差距的是能否讲清为什么选 Retrofit 而不是 HttpURLConnection为什么用 Room 而不是 SharedPreferences 存城市列表为什么天气图标要按 condition code 动态加载而非硬编码这套源码之所以被标注为“高分项目”核心在于它完整覆盖了 Android 开发中五个关键工程断点网络请求的健壮性封装、本地数据的结构化持久化、UI 层的响应式更新、后台任务的生命周期安全调度以及离线场景下的降级策略。它面向的是能通过答辩提问、能现场演示异常处理、能解释每个Override背后设计权衡的开发者而不是仅会粘贴代码的执行者。2. 从网络层开始Retrofit OkHttp 的组合为何成为毕设首选天气类 APP 对网络模块的要求看似简单实则暗藏多个易被忽略的工程细节API 响应格式不统一JSON 字段名大小写混用、服务端返回 HTTP 401 时需触发重新授权流程、弱网环境下需自动重试但不能无限循环、第三方天气接口常有调用频率限制。这些都不是AsyncTask JSONObject能优雅解决的。Retrofit 提供声明式接口定义OkHttp 提供拦截器链与连接池控制二者组合构成当前 Android 毕设中最可控、最易调试、最便于答辩展示的网络方案。2.1 定义 WeatherService 接口并处理字段兼容性天气 API如 OpenWeatherMap 或和风天气常存在字段命名不规范问题例如main.temp返回浮点数但文档未声明是否可能为 nullweather[0].description在部分城市返回空字符串sys.country可能全大写也可能小写。直接解析易崩溃。Retrofit 配合 GsonConverterFactory 时必须通过自定义 TypeAdapter 或注解处理public class WeatherResponse { SerializedName(main) private MainInfo main; SerializedName(weather) Expose private ListWeatherItem weatherList; SerializedName(name) private String cityName; // getter/setter 省略 } public class MainInfo { SerializedName(temp) private Double temperature; // 显式声明 Double 类型避免 int 强转异常 SerializedName(humidity) private Integer humidity; public Double getTemperature() { return temperature ! null ? temperature : 0.0; } }提示Expose注解配合GsonBuilder().excludeFieldsWithoutExposeAnnotation()可严格控制序列化范围避免因 JSON 多出字段导致解析失败——这是答辩时解释“如何保证接口变更不影响 APP”的关键点。2.2 构建带重试与日志拦截的 OkHttp Client毕设演示环境网络不稳定需在 OkHttp 层实现有限重试。注意不能在 Retrofit Call 中做重试会丢失原始请求上下文而应在 OkHttp 拦截器中实现private static OkHttpClient buildOkHttpClient() { return new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .retryOnConnectionFailure(true) // 启用基础重试 .addInterceptor(new LoggingInterceptor()) // 自定义日志拦截器 .addInterceptor(chain - { Request request chain.request(); Response response null; for (int i 0; i 2; i) { // 最多重试2次 try { response chain.proceed(request); if (response.isSuccessful() || response.code() 404) { break; // 成功或明确错误码不再重试 } } catch (IOException e) { if (i 2) throw e; // 最后一次失败抛出 } } return response; }) .build(); }2.2.1 日志拦截器必须输出真实请求/响应体答辩老师常会问“你怎么确认请求发出去了返回的数据对不对” 因此日志拦截器不能只打 URL 和状态码必须打印 body注意仅 DEBUG 模式启用且需判断Content-Type: application/jsonpublic class LoggingInterceptor implements Interceptor { Override public Response intercept(Chain chain) throws IOException { if (BuildConfig.DEBUG) { Request request chain.request(); long t1 System.nanoTime(); Response response chain.proceed(request); long t2 System.nanoTime(); String requestBody ; if (request.body() ! null) { Buffer buffer new Buffer(); request.body().writeTo(buffer); requestBody buffer.readUtf8(); } String responseBody ; if (response.body() ! null response.body().contentLength() 0) { ResponseBody body response.peekBody(1024 * 100); // 限制读取长度 responseBody body.string(); } Log.d(HTTP, String.format(↓ %s %s %.1fms\n→ %s\n← %d %s\n%s, request.method(), request.url(), (t2 - t1) / 1e6, requestBody, response.code(), response.message(), responseBody)); return response; } else { return chain.proceed(chain.request()); } } }注意peekBody()是安全读取响应体的关键避免body.string()导致后续无法读取——这是学生常踩的坑也是答辩中可展开的技术细节。3. 数据持久化Room 数据库如何替代 SharedPreferences 实现城市管理毕设中“记住用户添加的城市”功能90% 的学生用SharedPreferences存一个 JSON 字符串。但这种做法在答辩中极易被质疑“如果用户添加 50 个城市每次读写都全量解析 JSON性能怎么保障并发修改会不会丢数据” Room 提供编译期校验、异步 DAO、关系映射能力是体现工程规范性的核心模块。3.1 设计 CityEntity 与嵌套关系天气 APP 需支持“多城市切换”每个城市需记录名称、经纬度、是否设为默认、添加时间。Room 要求主键唯一且需支持按添加时间排序Entity(tableName cities) public class CityEntity { PrimaryKey(autoGenerate true) public long id; ColumnInfo(name city_name) public String cityName; ColumnInfo(name latitude) public double latitude; ColumnInfo(name longitude) public double longitude; ColumnInfo(name is_default) public boolean isDefault; ColumnInfo(name added_time) public long addedTime; // 时间戳用于排序 // 构造函数、getter/setter 省略 }3.2 DAO 接口必须包含事务与异步操作Room DAO 不仅是查询方法集合更是数据一致性的守门人。例如“设某城市为默认”需同时清除其他城市的is_default标记必须用Transaction保证原子性Dao public interface CityDao { Insert(onConflict OnConflictStrategy.REPLACE) long insert(CityEntity city); Query(SELECT * FROM cities ORDER BY added_time DESC) LiveDataListCityEntity getAllCities(); // 返回 LiveData自动绑定 UI 生命周期 Query(SELECT * FROM cities WHERE is_default 1 LIMIT 1) CityEntity getDefaultCity(); Query(UPDATE cities SET is_default 0 WHERE is_default 1) void clearDefaultFlag(); Transaction default void setDefaultCity(long cityId) { clearDefaultFlag(); updateDefaultFlag(cityId, true); } Query(UPDATE cities SET is_default :flag WHERE id :id) void updateDefaultFlag(long id, boolean flag); Query(DELETE FROM cities WHERE id :id) void deleteById(long id); }3.2.1 Database 初始化必须处理迁移与降级策略Room 默认不支持 schema 变更毕设开发中常因加字段导致崩溃。必须显式配置fallbackToDestructiveMigration()仅限调试阶段正式答辩演示前应使用addMigrations()public abstract class AppDatabase extends RoomDatabase { public abstract CityDao cityDao(); private static AppDatabase INSTANCE; public static AppDatabase getDatabase(final Context context) { if (INSTANCE null) { synchronized (AppDatabase.class) { if (INSTANCE null) { INSTANCE Room.databaseBuilder( context.getApplicationContext(), AppDatabase.class, weather_database ) .addMigration(MIGRATION_1_2) // 示例v1 → v2 增加 is_default 字段 .fallbackToDestructiveMigration() // 仅开发阶段启用 .build(); } } } return INSTANCE; } static final Migration MIGRATION_1_2 new Migration(1, 2) { Override public void migrate(NonNull SupportSQLiteDatabase database) { database.execSQL(ALTER TABLE cities ADD COLUMN is_default INTEGER NOT NULL DEFAULT 0); } }; }提示答辩时若被问“数据库升级怎么处理”可展示MIGRATION_1_2的 SQL 语句并说明fallbackToDestructiveMigration()仅用于快速验证逻辑实际交付需预置完整 migration 脚本——这直接体现工程素养。4. UI 层架构MVVM 模式下 ViewModel 如何协调网络与数据库毕设常见误区是把所有逻辑塞进 Activity导致代码不可测、不可复用、答辩时无法清晰拆解职责。MVVM 中 ViewModel 是业务逻辑中枢它不持有 View 引用通过LiveData或StateFlow向 UI 发送状态同时协调 Repository数据仓库完成网络请求与本地缓存协同。4.1 构建 WeatherRepository 统一数据源Repository 模式隔离数据来源使 ViewModel 不关心数据来自网络还是数据库。天气 APP 典型场景优先显示缓存数据提升首屏速度再异步刷新网络最新值public class WeatherRepository { private final WeatherApiService apiService; private final CityDao cityDao; private final WeatherDao weatherDao; public WeatherRepository(WeatherApiService apiService, CityDao cityDao, WeatherDao weatherDao) { this.apiService apiService; this.cityDao cityDao; this.weatherDao weatherDao; } public LiveDataWeatherData getWeatherForCity(String cityName) { MutableLiveDataWeatherData result new MutableLiveData(); // 步骤1立即返回缓存 WeatherData cached weatherDao.getWeatherByCityName(cityName); if (cached ! null) { result.setValue(cached); } // 步骤2发起网络请求成功后更新缓存并推送新数据 apiService.getWeatherByCity(cityName).enqueue(new CallbackWeatherResponse() { Override public void onResponse(CallWeatherResponse call, ResponseWeatherResponse response) { if (response.isSuccessful() response.body() ! null) { WeatherData newData convertToWeatherData(response.body()); weatherDao.insert(newData); // 写入缓存 result.postValue(newData); // 推送至 UI } else { // 网络失败时仍保留缓存数据体现离线可用性 result.postValue(cached); } } Override public void onFailure(CallWeatherResponse call, Throwable t) { result.postValue(cached); } }); return result; } }4.2 ViewModel 必须暴露明确的状态流与事件流ViewModel 不应暴露MutableLiveData给 UI而应提供不可变的LiveData用于状态另用SingleLiveEvent或EventWrapper处理一次性事件如 Toast 提示、页面跳转public class WeatherViewModel extends ViewModel { private final WeatherRepository repository; private final MutableLiveDataString cityInput new MutableLiveData(); private final MutableLiveDataBoolean isLoading new MutableLiveData(); private final MutableLiveDataString errorMessage new MutableLiveData(); // 状态流天气数据 private final LiveDataWeatherData weatherData; // 事件流添加城市成功后需刷新列表 private final SingleLiveEventVoid refreshCityList new SingleLiveEvent(); public WeatherViewModel(WeatherRepository repository) { this.repository repository; this.weatherData Transformations.switchMap(cityInput, repository::getWeatherForCity); } public void loadWeather(String cityName) { isLoading.setValue(true); cityInput.setValue(cityName); } public void addCity(CityEntity city) { new InsertAsyncTask(cityDao).execute(city); refreshCityList.call(); // 触发 UI 刷新城市列表 } // getter 方法 public LiveDataWeatherData getWeatherData() { return weatherData; } public LiveDataBoolean getIsLoading() { return isLoading; } public LiveDataString getErrorMessage() { return errorMessage; } public SingleLiveEventVoid getRefreshCityList() { return refreshCityList; } }4.2.1 UI 层绑定必须遵循生命周期安全原则Activity 中观察LiveData时必须传入this即 LifecycleOwner否则可能引发内存泄漏或空指针Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); viewModel new ViewModelProvider(this).get(WeatherViewModel.class); // ✅ 正确自动解注册 viewModel.getWeatherData().observe(this, weather - { binding.tvTemp.setText(String.format(%.1f°C, weather.getTemperature())); binding.tvDesc.setText(weather.getDescription()); }); // ❌ 错误传入 getApplication() 将导致永远不解除观察 }注意SingleLiveEvent是解决LiveData重复回调的经典方案其原理是继承MutableLiveData并重写observe()内部维护mPending标志位。毕设中若用到跳转或弹窗必须使用此类封装——这是区分“能跑”和“专业”的分水岭。5. 离线与异常处理让天气 APP 在无网时依然可用高分项目与普通项目的本质差异在于对边界条件的处理深度。天气 APP 最典型的边界是启动时无网络、城市列表为空、API 返回 404城市不存在、GPS 定位失败。这些场景不能简单弹 Toast而应有降级路径与用户引导。5.1 启动时无网络强制展示缓存城市与手动输入入口Application.onCreate()中检查网络状态若不可用则禁用自动定位并提示用户手动添加public class WeatherApplication extends Application { Override public void onCreate() { super.onCreate(); if (!isNetworkAvailable()) { // 初始化时跳过自动定位逻辑 PreferenceManager.getDefaultSharedPreferences(this) .edit().putBoolean(auto_location_enabled, false).apply(); } } private boolean isNetworkAvailable() { ConnectivityManager cm (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork cm.getActiveNetworkInfo(); return activeNetwork ! null activeNetwork.isConnectedOrConnecting(); } }UI 层对应逻辑当viewModel.getWeatherData().getValue() null且网络不可用时显示“暂无网络可手动添加城市”按钮并禁用下拉刷新。5.2 城市不存在的友好提示与纠错建议OpenWeatherMap API 对无效城市返回 HTTP 404此时不应静默失败而应解析响应体中的message字段并给出具体建议// 在 Retrofit Callback 的 onFailure 中 if (response.code() 404) { try { JSONObject errorJson new JSONObject(response.errorBody().string()); String errorMsg errorJson.optString(message, 城市未找到); if (errorMsg.contains(invalid city)) { errorMsg 请输入正确的城市名称如北京、Shanghai; } errorMessage.postValue(errorMsg); } catch (Exception e) { errorMessage.postValue(城市信息有误请检查输入); } }5.3 GPS 定位失败时的三级降级策略定位失败不能直接报错应按优先级尝试读取上次成功定位的城市SharedPreferences存 last_city读取 Room 中最近添加的城市显示中国默认城市北京private void tryGetLocation() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { LocationManager lm (LocationManager) getSystemService(LOCATION_SERVICE); if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) { lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener); } else { fallbackToLastCity(); // 降级策略入口 } } } private void fallbackToLastCity() { String lastCity PreferenceManager.getDefaultSharedPreferences(this) .getString(last_city, ); if (!lastCity.isEmpty()) { viewModel.loadWeather(lastCity); return; } // 查询 Room 中最近添加的城市 cityDao.getLastAddedCity().observe(this, city - { if (city ! null) { viewModel.loadWeather(city.getCityName()); } else { viewModel.loadWeather(Beijing); // 终极降级 } }); }提示答辩时可强调“三级降级”设计思想不依赖单一数据源每一层失败都有明确兜底且用户全程可见反馈——这正是工业级 APP 与玩具 Demo 的分界线。6. 毕设答辩高频问题应对与源码验证技巧高分项目源码的价值不仅在于能运行更在于能经得起追问。以下三个技巧可快速验证源码质量并预判答辩问题6.1 用 adb 命令验证数据库内容是否真实写入不要只信 Logcat直接查 SQLite 文件# 进入设备 shell adb shell # 切换到应用私有目录需 root 或 debuggable APK cd /data/data/com.example.weatherapp/databases/ ls -l # 查看 weather_database 文件时间戳 # 导出数据库到电脑 adb pull /data/data/com.example.weatherapp/databases/weather_database ~/Desktop/ # 用 DB Browser for SQLite 打开执行 SELECT * FROM cities; 验证数据关键点若weather_database文件大小始终为 0KB说明 Room 初始化失败或 DAO 调用未触发——这是答辩时“为什么城市没保存”的标准排查路径。6.2 模拟网络异常检验重试与降级逻辑使用 Android Studio 的 Network Profiler 或第三方工具如 Charles Proxy主动断开网络观察第一次加载是否显示“加载中”动画isLoading为 true重试期间是否持续显示 loading非闪退最终是否回退到缓存数据或默认城市若直接 Crash 或空白页则onFailure()未正确处理。6.3 检查 ProGuard/R8 混淆是否破坏 Gson 解析Release 版本常因混淆导致 JSON 解析失败。必须在proguard-rules.pro中保留 Retrofit 与 Gson 相关类# Retrofit 2.X -keep class retrofit2.** { *; } -dontwarn retrofit2.** # Gson -keep class com.google.gson.** { *; } -keep class com.example.weatherapp.model.** { *; } # 替换为你的包名 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); }验证方法生成 Release APK 后用unzip -l app-release.apk | grep model确认实体类未被移除再真机安装测试天气加载。最后提醒源码中所有TODO、FIXME注释必须清理干净Log.d()在 Release 版本应全部移除或用BuildConfig.DEBUG包裹AndroidManifest.xml中android:debuggabletrue必须改为false——这些细节往往是答辩老师翻源码时最先注意到的扣分点。本文还有配套的精品资源点击获取
返回列表