Android天气预报App课设全攻略:从Retrofit网络请求到Room数据缓存
2026/9/16 1:22:54 网站建设 项目流程

简介:Android Studio实现的天气预报App完整工程源码,面向Android初学者及课程设计、大作业场景,帮助快速搭建一个可运行的网络数据解析应用。项目调用中国天气网API,通过HTTP获取网络信息,用JSON解析气象数据并展示到界面;同时使用数据库保存城市及其URL,支持城市收藏与一键切换。资源共107个文件,以png图片、xml布局与配置、java逻辑代码为主,另含gradle构建脚本、jar依赖及properties配置等,压缩包约6.75MB。已有532人学习下载,适合作为Android网络编程、数据解析与本地存储的综合练手项目。源码结构完整,可直接导入Android Studio运行,便于对照学习HTTP请求封装、JSON解析、SQLite操作及多界面切换等关键知识点。

1. 为什么“天气预报App”是课设名单里最值得做透的题目

如果你翻过历年计算机专业课设题目清单,会发现天气预报App几乎是永不缺席的那一个。它看起来简单——无非是拉个接口、解析JSON、显示温度,但真正动手后你才会意识到,一个能打的课设版本要同时处理网络请求、XML/JSON解析、多线程切换、UI状态管理、定位权限、生命周期适配和异常降级。这些恰好覆盖了Android开发面试高频考点的七成以上。本文就按“拿源码能跑通、被追问能讲清”的标准,带你从零搭建一个可以直接交差的天气预报App,顺带说清楚每个模块为什么这么设计。适合正在做课设、准备复试作品集、或想快速掌握Android网络层写法的开发者阅读。

2. 开题前的技术选型:接口、架构与Gradle配置

2.1 天气数据源怎么选:免费API的对比与取舍

做天气预报App第一步不是写代码,而是选数据源。课设场景下你不可能去买商业气象数据,业内最常见的方案有三种:和风天气(QWeather)、OpenWeatherMap、以及聚合数据这类第三方平台。我个人更推荐和风天气的免费开发者版,原因很直接:国内访问稳定、文档全、返回的JSON结构对中国城市名友好,而且免费额度对课设演示完全够用。

对比项和风天气OpenWeatherMap聚合数据
国内访问速度偶尔超时
免费额度每日约1000次每分钟60次每月100次
返回字段中英双语英文为主中文
是否需要注册需要需要需要
城市匹配支持拼音/中文/经纬度城市ID城市名

选好源之后,务必去控制台创建项目并拿到API Key。这个Key就是你的身份凭证,课设答辩时老师大概率会问“Key泄漏了会怎样”,标准答案是:Key写在客户端里本身就是不安全的设计,正式产品应通过服务端转发请求,但课设阶段为了方便演示,直接放本地并配合ProGuard混淆即可。

2.2 架构选型:单Activity多Fragment还是多Activity

很多课设源码喜欢一个页面堆到底,全部逻辑塞进MainActivity,代码动辄两千行。这种写法不是说不能跑,而是答辩时老师一眼就能看出你没有工程意识。我建议采用单Activity + 多Fragment结构,再配一个轻量级的ViewModel层。

常见做法是:

  • MainActivity只负责承载Fragment、申请运行时权限、监听网络状态
  • WeatherFragment负责展示当日天气
  • ForecastFragment负责展示未来几天预报
  • CityManageFragment负责城市增删和排序

这样拆的好处是,每个类的职责清晰,后续加功能不会牵一发动全身。更重要的是,在答辩的“请介绍一下你的项目架构”环节,你能用两三句话讲明白数据流方向:Fragment发起请求 → ViewModel持有状态 → Repository层统一管理数据来源(网络/缓存)→ 数据通过LiveData回传 → UI层观察并渲染。

2.3 Gradle配置:build.gradle里的关键依赖与参数

不管Android Studio版本如何迭代,build.gradle的配置思路是不变的。下面是一份课设项目最常见的依赖集合,已去掉过时的库:

plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'com.example.weatherapp' compileSdk 34 defaultConfig { applicationId "com.example.weatherapp" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } buildFeatures { viewBinding true } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } }

依赖部分再单独说明。Retrofit2负责网络请求,Gson负责JSON解析,ViewModel与LiveData负责生命周期安全的数据持有,Room负责本地缓存,这三个库的组合在课设里足够体面:

dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // 网络 implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' // 生命周期 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' // 本地缓存 implementation 'androidx.room:room-runtime:2.6.1' kapt 'androidx.room:room-compiler:2.6.1' }

compileSdk 34对应Android 14,targetSdk 34意味着你要处理Android 13以后的通知权限和精确闹钟权限,课设阶段不需要精确闹钟,但天气预警的通知弹出在Android 13+设备上需要动态申请POST_NOTIFICATIONS权限。minSdk选24是为了覆盖Android 7.0以上设备,如果你的测试机是旧机型,可以适当下调,但不建议低于23,否则运行时权限模型会让你多写一套兼容逻辑。

3. 网络层与数据解析:从Retrofit到JSON的完整链路

3.1 定义API接口:Retrofit注解的正确打开方式

选好依赖后,第一步是写一个接口类,告诉Retrofit“你要请求什么地址、参数叫什么”。和风天气的免费版API路径为/v7/weather/now,传城市ID就能拿到实时天气。课设里我一般建议同时接两个接口:/v7/weather/now拿当天实况,/v7/weather/3d拿未来三天预报,这样你的App信息量更足,也顺势演示了多接口的封装能力。

public interface WeatherApiService { // 实时天气 @GET("v7/weather/now") Call<WeatherResponse> getNowWeather( @Query("location") String locationId, @Query("key") String apiKey ); // 未来三天预报 @GET("v7/weather/3d") Call<ForecastResponse> getForecast( @Query("location") String locationId, @Query("key") String apiKey ); }

这段代码里的@GET声明了HTTP方法,括号内填的是相对路径,域名部分在创建Retrofit实例时统一指定。@Query注解会把参数拼接到URL后面,最终请求的格式是https://devapi.qweather.com/v7/weather/now?location=101010100&key=你的Key。location字段在免费版里既可以传城市ID,也可以直接传经纬度—课设里传城市ID更直观,数字不易出差错。

3.2 创建Retrofit单例与OkHttp拦截器

接口定义好了,还需要一个“发动机”来驱动它。Retrofit单例的写法很多,我建议用双重检查锁,兼顾线程安全与性能。同时加上日志拦截器,调试时能在Logcat里看到完整的请求和响应内容,这对课设答辩时演示“我排查过一个接口返回异常的问题”非常有用。

public class NetworkClient { private static final String BASE_URL = "https://devapi.qweather.com/v7/"; private static volatile WeatherApiService service; public static WeatherApiService getService() { if (service == null) { synchronized (NetworkClient.class) { if (service == null) { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor.Level.BASIC)) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); service = retrofit.create(WeatherApiService.class); } } } return service; } }

超时时间设置要解释清楚:connectTimeout是建立TCP连接的最长等待时间,readTimeout是服务端返回数据的间隔上限。课设里我习惯都设10秒,因为Wi-Fi环境下正常接口500毫秒内就能返回,10秒足够宽松;如果你用了国外数据源,建议把readTimeout提到15秒,但connectTimeout最好别超过10秒,否则弱网环境下用户会感觉App“卡死了”。

3.3 解析JSON:用Gson还是手动解析

Retrofit的converter-gson会自动把响应体转成Java对象,所以这一步你真正要做的是设计好“数据模型类”。和风天气的JSON结构是这样的:

{ "code": "200", "updateTime": "2024-06-01T10:00+08:00", "now": { "temp": "28", "text": "多云", "windDir": "东南风", "windScale": "3级", "humidity": "52%" } }

对应的Java实体类可以这样写,注意字段名必须和JSON里的key完全一致,否则Gson解析出来就是null:

public class WeatherResponse { private String code; private String updateTime; private Now now; public String getCode() { return code; } public String getUpdateTime() { return updateTime; } public Now getNow() { return now; } public static class Now { private String temp; private String text; private String windDir; private String windScale; private String humidity; } }

这里的嵌套类Now对应JSON里的now对象。如果json字段是下划线风格——某些API叫temp_chumidity这种,Gson会解析失败,你就需要在字段上加@SerializedName("temp_c")注解来显式声明映射。我们用的是和风天气,字段没有下划线,但面试官极大概率会追问“如果API字段是下划线,你怎么处理”,能答出SerializedName就算过。

3.4 Repository层:把网络与缓存解耦

前面搭好的网络层不能直接丢给Activity用。更好的做法是加一个Repository类,它负责决定“当前数据是从网络拿,还是从Room缓存拿”。课设项目未必需要严格的Clean Architecture,但至少要在Repository层做一次缓存判断:

public class WeatherRepository { private final WeatherApiService apiService; private final WeatherDao weatherDao; public WeatherRepository(WeatherApiService apiService, WeatherDao weatherDao) { this.apiService = apiService; this.weatherDao = weatherDao; } public LiveData<WeatherResult> getWeather(String cityId) { MutableLiveData<WeatherResult> result = new MutableLiveData<>(); // 先尝试从数据库读取缓存 WeatherEntity cached = weatherDao.getByCityId(cityId); if (cached != null && isFresh(cached.getUpdateTime())) { result.postValue(WeatherResult.success(cached)); return result; } // 缓存失效则请求网络 apiService.getNowWeather(cityId, ApiConfig.KEY).enqueue(new Callback<WeatherResponse>() { @Override public void onResponse(Call<WeatherResponse> call, Response<WeatherResponse> response) { if (response.isSuccessful() && response.body() != null) { WeatherEntity entity = convertToEntity(response.body(), cityId); weatherDao.insert(entity); result.postValue(WeatherResult.success(entity)); } else { result.postValue(WeatherResult.error("接口返回异常: " + response.code())); } } @Override public void onFailure(Call<WeatherResponse> call, Throwable t) { result.postValue(WeatherResult.error(t.getMessage())); } }); return result; } private boolean isFresh(String updateTime) { // 缓存10分钟内有效 try { long cachedTime = TimeUtil.parse(updateTime); return System.currentTimeMillis() - cachedTime < 10 * 60 * 1000; } catch (Exception e) { return false; } } }

这段代码里enqueue方法是Retrofit的异步回调,它会在子线程执行网络请求,然后回调到主线程,所以你在onResponse里操作Room数据库是安全的。postValue用于子线程,setValue只能主线程调用,二者不能混用,这也是LiveData最常见的一个坑——子线程里用了setValue直接崩溃,日志会报java.lang.IllegalStateException: Cannot invoke setValue on a background thread

4. 主界面渲染与多城市管理:RecyclerView + Room的实战组合

4.1 卡片式天气列表:RecyclerView的Adapter写法

主界面用RecyclerView展示每日天气卡片是最常见的UI方案。每个item展示日期、天气图标、最高温、最低温。这里不展开布局XML的细节,只挑Adapter里的关键逻辑说:

public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.ViewHolder> { private List<DailyForecast> dataList = new ArrayList<>(); public void submitList(List<DailyForecast> list) { this.dataList = list; notifyDataSetChanged(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_forecast, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { DailyForecast item = dataList.get(position); holder.tvDate.setText(item.getFxDate()); holder.tvMaxTemp.setText(item.getTempMax() + "℃"); holder.tvMinTemp.setText(item.getTempMin() + "℃"); holder.tvText.setText(item.getTextDay()); } @Override public int getItemCount() { return dataList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvDate, tvMaxTemp, tvMinTemp, tvText; ViewHolder(View itemView) { super(itemView); tvDate = itemView.findViewById(R.id.tv_date); tvMaxTemp = itemView.findViewById(R.id.tv_max_temp); tvMinTemp = itemView.findViewById(R.id.tv_min_temp); tvText = itemView.findViewById(R.id.tv_text); } } }

这段代码里notifyDataSetChanged()可以工作,但不是最佳实践。如果你追求的是一个拿得出手的课设,建议改成ListAdapter配合DiffUtil,这样列表更新时可以精准定位变化的item,而不是整表刷新。DiffUtil的写法不复杂,重写areItemsTheSameareContentsTheSame两个方法即可,但很多课设源码图省事用的是直接notify,答辩时你如果能主动说出DiffUtil的应用场景,印象分会明显高一个档次。

4.2 多城市管理:Room数据库的表设计与增删改查

课设要求里如果有“支持多城市切换”这一条,你就必须引入本地数据库。SQLite虽好,但手写SQLiteOpenHelper对新手不友好,Room是Jetpack家族中的ORM框架,编译期帮你校验SQL语句,跑错了能提前报错而非运行时崩溃。下面是WeatherEntity的定义:

@Entity(tableName = "weather_cache") public class WeatherEntity { @PrimaryKey @NonNull private String cityId; private String cityName; private String temp; private String text; private String updateTime; private long lastRefreshTime; // 省略getter/setter }

Dao层接口也不复杂,四个方法覆盖了课设最常见操作:按城市查、全部城市、插入更新、删除:

@Dao public interface WeatherDao { @Query("SELECT * FROM weather_cache ORDER BY lastRefreshTime DESC") LiveData<List<WeatherEntity>> getAllCities(); @Query("SELECT * FROM weather_cache WHERE cityId = :cityId") WeatherEntity getByCityId(String cityId); @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(WeatherEntity entity); @Delete void delete(WeatherEntity entity); }

LiveData返回类型写在Dao方法里是Room的一个特性——数据库变化时会自动通知观察者刷新UI,这意味着你新增城市后列表无需手动刷新。OnConflictStrategy.REPLACE表示主键冲突时用新数据覆盖旧数据,这个策略用来处理“同一个城市反复下拉刷新”的场景再合适不过。

4.3 下拉刷新与空态处理:SwipeRefreshLayout的接入细节

主界面建议套上一层SwipeRefreshLayout,用户下拉即可手动触发刷新。接入时有几个细节容易踩坑:

  • 监听器里必须判断isRefreshing(),避免连续下拉触发多个请求
  • 请求结束后无论成功失败都要调用swipeRefreshLayout.setRefreshing(false)
  • 弱网环境下请求可能超过10秒,要有个进度提示,否则用户会一直下拉看转圈
swipeRefreshLayout.setOnRefreshListener(() -> { if (swipeRefreshLayout.isRefreshing()) { return; } viewModel.refreshWeather(currentCityId); });

另外,别忘了处理“首次进入没有任何缓存”的空态。很多课设App在数据库为空时直接显示一个空白页,这在演示时非常尴尬。标准做法是在布局里放一个TextView提示“下拉添加城市”或者自动弹出城市选择Dialog。

5. 交付课设的加分技巧:动态图标、混淆与答辩验证点

到了这一章,核心功能已经完整:网络请求能通、数据能解析、UI能渲染、多城市能增删。接下来要做的不是躺平,而是往“优秀课设”的层次再推几步。第一个值得花时间的是天气图标动态化。不要只显示文字,而是根据天气现象匹配对应的矢量图标——和风天气返回的text字段是中文(比如“多云”“小雨”),你需要在代码里做一层映射:

public static int getWeatherIconRes(String text) { switch (text) { case "晴": return R.drawable.ic_sunny; case "多云": return R.drawable.ic_cloudy; case "小雨": case "中雨": return R.drawable.ic_rain; case "雪": return R.drawable.ic_snow; default: return R.drawable.ic_default; } }

这段映射放工具类里,所有用到天气图标的地方统一调用。第二个加分项是对Release包做混淆验证。课设源码放在zip里发给老师或上传GitHub时,保不齐别人会拿你的包反编译看代码。ProGuard规则里把API Key处理掉,或者提示用户自行申请,这才是负责任的做法:

# 保留数据模型类的字段,否则Gson解析会失败 -keep class com.example.weatherapp.model.** { *; }

最后一个答辩验证点列表建议你提前掌握,老师在演示时大概率会问的动作:冷启动App是否能从Room缓存快速渲染;飞行模式下拉刷新是否给出“网络不可用”的提示;连续切换城市是否出现数据错乱。这三个点对应的都是实打实的代码逻辑,都能直接讲出你的防御设计。把这些记下来,你的课设就不再是飘在表面的“能用”,而是扎实的“能讲、可维护”。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询