Android Studio天气预报App课设:从定位到上屏的完整实现

📅 发布时间:2026/10/10 6:53:37
Android Studio天气预报App课设:从定位到上屏的完整实现
简介Android Studio实现的天气预报App项目源码面向Android初、中级学习者及需要完成课程设计或毕业设计的学生。项目采用中国天气网API通过HTTP获取网络数据、JSON解析并展示到界面数据库负责存放城市与对应URL支持城市收藏及切换天气是一套功能完整的可运行课设方案。源码包共107个文件涵盖15个Java逻辑文件、24个XML布局与配置、53个PNG界面资源另含gradle构建文件、pinyin4j.jar等依赖压缩包仅6.75MB结构清晰便于直接导入Android Studio使用。当前已有532人学习下载。通过该项目可以系统掌握网络请求、JSON解析、SQLite存储和动态界面刷新等核心开发流程也可作为毕业设计、课程作业的参考模板或二次开发基础。1. Android Studio实现天气预报App这个课设题目的关键不在写代码Android Studio实现天气预报App几乎是Android方向课设里点名率最高的题目之一。但大部分人的进度卡在同一个位置代码能从 GitHub 或网盘下下来一 Sync 就报错报错修好了一运行又闪退闪退解决了定位权限弹窗出来后天气数据还是空的。原因不是代码写得差而是这条链路由四段组成——定位、联网、解析、上屏任何一段出问题界面上就是一片空白。这个标题对应的不只是一份能运行的工程而是一份按课设评分点整理过的完整项目入口界面、网络请求模块、JSON解析Bean、多日天气列表、权限申请都在里面。你要做的不是从零写一遍而是把它的结构吃透然后按自己老师的要求改包名、改UI、改数据源再能在答辩现场把每个模块讲清楚。我接下来会按拿到这类工程后的处理顺序来讲先定三件容易被忽略的前置事再走一遍核心链路的实现最后把高频坑和答辩前的自测方法补上。适合两类人准备交Android大作业的学生以及想拿现成工程做二次开发、但不想被环境问题卡住的人。2. 跑通天气App前的三件事环境、数据源、工程骨架2.1 环境匹配AGP 与 Gradle 版本对不上Sync 永远是红色拿到工程的第一件事不是点 Run而是先确认这个项目原本是在什么环境上跑的。天气课设项目大多是从 Android Studio 里直接导出的 zip里面会带着gradle-wrapper.properties和根目录build.gradle。这两个文件决定了项目要用哪个 Gradle 版本和 Android Gradle PluginAGP版本。大部分 Sync 报错、依赖下载失败、构建卡在 90% 的问题根源都是版本错配。先看gradle-wrapper.properties里锁定的 Gradle 版本distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists再对照根目录build.gradle里的 AGP 版本buildscript { dependencies { classpath com.android.tools.build:gradle:8.4.0 } }版本匹配关系决定了 Sync 能不能过。常见的做法是AGP 8.x 配 Gradle 8.xAGP 7.x 配 Gradle 7.x。如果你本地的 Android Studio 版本比较新但项目用的还是 AGP 7.xSync 时它会提示升级反过来本地 Gradle 版本太旧AGP 8.x 会直接报AGP requires Gradle X并停止构建。工程里的 AGP 版本最低 Gradle 版本对应 JDKAGP 7.xGradle 7.xJDK 11AGP 8.xGradle 8.xJDK 17我一般拿到工程后先做两件事第一确认gradle-wrapper.properties里的 distributionUrl 是不是国内能正常下载的地址不是的话改成或手动下载对应 zip 放到 Gradle 用户目录第二确认 JDK 用的是 Android Studio 自带的 JBRJetBrains Runtime不要在系统里额外配一个 JDK 8 或 JDK 11否则会冒出各种 Unsupported class file major version 的诡异报错。提示不要因为 Sync 报错就盲目把 Gradle 版本往上升。AGP 和 Gradle 是一一对应的改一个不换另一个只会从红色报错变成另一个红色报错。2.2 天气数据源选型免费 Key、URL 请求格式、返回 JSON 结构Weather App 的核心不是界面是数据源。选数据源的原则有三条免费额度够课设演示、接口不需要 OAuth 那种复杂的授权流程、返回的 JSON 结构稳定且字段完整。我习惯用某天气数据平台的免费版它按城市 ID 查询实时天气和未来几天预报key 直接拼在 URL 上没有签名算法最适合课设和二次开发。这类接口的请求格式通常长这样GET http://api.weather.example.com/v1/weather/now?cityIdCITY_IDkeyYOUR_KEY返回的 JSON 大概是这样注意嵌套层级{ code: 200, cityId: 101010100, now: { temp: 26, text: 多云, windDir: 东南风, windScale: 3 }, forecast: [ { date: 2025-01-06, tempMax: 30, tempMin: 22, textDay: 晴 } ] }搞清楚每个字段的含义比急着写代码更重要。这决定了后面 JSON 解析的 Bean 长什么样请求参数说明示例cityId城市编号不是城市名101010100key数据平台分配的免费密钥一串十六进制字符串v1/weather/now实时天气接口路径固定不变免费版通常有每日调用次数限制课设演示时不会触发但要注意一个细节不要在代码里写死循环去轮询也不要每次进页面都重新申请 key。数据源选好后把 key 提到一个常量类里别散落在 Activity 里后面改起来方便。2.3 工程目录结构先认骨架再动手改解压这个 zip 后正常情况下你会看到一个标准的 Android 工程目录。我拿到这类项目后会先把目录按模块过一遍不急着打开 MainActivity 看逻辑。因为天气 App 的结构太固定了认清了每个模块的职责后面改代码时才知道去哪找。app/src/main/java/com/example/weather/ ├── MainActivity.java # 入口负责初始化、权限、刷新逻辑 ├── adapter/ │ └── WeatherAdapter.java # 多日天气列表的适配器 ├── network/ │ └── HttpUtil.java # OkHttp 请求封装 ├── bean/ │ └── WeatherBean.java # Gson 解析用的数据类 └── res/ ├── layout/ │ ├── activity_main.xml # 主界面布局 │ └── item_weather.xml # 单日天气卡片 └── values/ ├── strings.xml └── colors.xml入口 Activity 负责把权限、定位、网络请求、UI 更新串起来HttpUtil 只管发请求和拿回 JSON 字符串WeatherBean 只负责把 JSON 映射成 Java 对象WeatherAdapter 只负责把数据画到列表上。各模块之间用最简单的方式调用不要搞复杂的架构课设项目最怕过度设计。拿到工程后我建议先做一个容易被忽略的操作改包名。右键项目名 → Refactor → Rename → 把com.example.weather改成你自己的域名形式。这一步不涉及代码逻辑但意义重大。同一届里一旦有两个人交了包名一样的项目老师那边查重时解释成本会非常高。顺便把applicationId也同步改掉这一步做完项目才真的算你的。3. 把核心链路串起来定位、请求、解析、上屏3.1 Android 6.0 动态权限申请定位和联网缺一不可天气 App 需要三个权限网络权限、粗略定位权限、精确定位权限。网络权限是普通权限在 Manifest 里声明即可定位权限属于危险权限Android 6.0 及以上必须在运行时动态申请否则就算你在 Manifest 里写了应用也拿不到定位结果。先在AndroidManifest.xml里声明uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /然后在 MainActivity 里写运行时请求private void requestLocationPermission() { if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, 1001); } else { getLocation(); } } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode 1001 grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { getLocation(); } }这段代码的逻辑很直接先检查自己有没有定位权限没有就弹系统对话框有就直接去拿定位。requestCode传 1001回调时用同一个值判断是哪次请求的结果。要注意的是别把网络权限也放进requestPermissions里一起申请普通权限不需要运行时弹窗也不应该出现在这个列表里。提示Android 12 及以上还多了一个精确位置开关。模拟器上默认可能只给了模糊定位实测发现拿不到坐标时先去系统设置里确认位置权限是精确而不是大致。3.2 用 OkHttp 封装天气请求URL 构造与回调线程切换网络请求我习惯用 OkHttp。相比 HttpURLConnection它的 API 更简洁超时设置、异步回调、连接池都是开箱即用。课设项目里不需要引入 Retrofit那会平白多一层注解和适配器答辩时反而不好解释。封装一个 HttpUtil核心就一个方法传入 URL回调里返回 JSON 字符串。public class HttpUtil { private static final OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); public static void get(String url, Callback callback) { Request request new Request.Builder() .url(url) .get() .build(); client.newCall(request).enqueue(callback); } }调用时注意回调线程。enqueue的回调跑在 OkHttp 的子线程里不能直接更新 UIHttpUtil.get(url, new Callback() { Override public void onFailure(Call call, IOException e) { runOnUiThread(() - showError(网络请求失败)); } Override public void onResponse(Call call, Response response) throws IOException { String json response.body().string(); runOnUiThread(() - parseAndShow(json)); } });connectTimeout是建立连接的超时时间readTimeout是拿到响应体的超时时间课设场景下各设 10 秒足够。runOnUiThread是把子线程切回主线程的标准姿势不用它就会崩在Only the original thread that created a view hierarchy can touch its views这个异常上。有个细节值得注意response.body().string()只能调用一次多次调用会拿到空字符串。很多人在这里踩坑以为接口返回了空数据其实是自己把 Response Body 读了两遍。3.3 Gson 解析嵌套 JSON字段对照和兜底默认值拿到 JSON 字符串之后下一步是解析成 Java 对象。这里我用 Gson配合SerializedName注解把 JSON 字段名映射成 Java 字段名。不用注解也可以但要求 Java 字段名和 JSON 字段名完全一致课设代码里还是注解更直观。WeatherBean 按返回结构拆成嵌套类public class WeatherBean { SerializedName(code) public String code; SerializedName(cityId) public String cityId; SerializedName(now) public Now now; SerializedName(forecast) public ListForecast forecast; public static class Now { SerializedName(temp) public String temp; SerializedName(text) public String text; SerializedName(windDir) public String windDir; } public static class Forecast { SerializedName(date) public String date; SerializedName(tempMax) public String tempMax; SerializedName(tempMin) public String tempMin; SerializedName(textDay) public String textDay; } }解析那一行的代码很简单WeatherBean bean new Gson().fromJson(json, WeatherBean.class);但简单的前提是字段对得上。我把常见的对应关系列出来JSON 字段Bean 字段类型说明now.tempnow.tempString当前温度now.textnow.textString当前天气现象forecast[0].tempMaxforecast.get(0).tempMaxString当天最高温forecast[0].textDayforecast.get(0).textDayString白天天气解析时另一个要点是判空。后端偶尔会把now或forecast返回成 null直接用bean.now.temp必然空指针。我习惯在 Bean 里给所有字段设默认值或者在解析完后加一层if (bean.now ! null)保护。3.4 RecyclerView 展示多日天气把 Bean 变成卡片实时天气那一块用 TextView 显示没问题多日天气预报就必须上 RecyclerView 了。用 ScrollView 套横向 LinearLayout 虽然也能跑但滑动性能和复用机制差很多答辩时老师一问为什么用 RecyclerView你还能顺带讲出 ViewHolder 复用机制这个考点。MainActivity 里初始化列表的代码RecyclerView recyclerView findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setHasFixedSize(true); weatherAdapter new WeatherAdapter(forecastList); recyclerView.setAdapter(weatherAdapter);setHasFixedSize(true)的作用是告诉 RecyclerViewitem 的尺寸不会因为内容变化而改变让它跳过重新测量这一步滑动会明显更流畅。Adapter 的核心逻辑在onBindViewHolder里public class WeatherAdapter extends RecyclerView.AdapterWeatherAdapter.ViewHolder { private ListWeatherBean.Forecast list; public WeatherAdapter(ListWeatherBean.Forecast list) { this.list list; } NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_weather, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { WeatherBean.Forecast item list.get(position); holder.dateView.setText(item.date); holder.tempView.setText(item.tempMin ~ item.tempMax ℃); holder.textView.setText(item.textDay); } Override public int getItemCount() { return list null ? 0 : list.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView dateView, tempView, textView; ViewHolder(NonNull View itemView) { super(itemView); dateView itemView.findViewById(R.id.tv_date); tempView itemView.findViewById(R.id.tv_temp); textView itemView.findViewById(R.id.tv_text); } } }onCreateViewHolder负责创建 item 的视图容器getItemCount返回列表长度onBindViewHolder是每次滚动到某个位置时把数据填进对应控件。注意getItemCount里要做 null 保护因为网络数据没回来时forecastList可能是空的返回 0 就不会画任何东西也不会崩。4. 天气课设App避坑手册5个高频翻车点的现象、原因、解决4.1 模拟器上定位失败天气永远显示默认城市现象真机上能定位模拟器上运行却永远显示默认城市。不管怎么点击刷新城市都不变。原因模拟器的 GPS 默认没有坐标值。Android 模拟器不像手机一样有真实的 GPS 模块LocationManager 拿到的 location 对象可能为空代码里直接getLatitude()就抛空指针或者拿到一个默认的 0,0 坐标。解决两步。第一步在模拟器的 Extended Controls 里找到 Location 面板手动填入一组经纬度比如你所在城市的坐标点 Send 发送给模拟器。第二步代码里对 location 做判空处理拿不到坐标时直接用默认城市 ID 发起请求。判空代码放在定位回调里Location location locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); String cityId DEFAULT_CITY_ID; if (location ! null) { cityId convertLocationToCityId(location.getLatitude(), location.getLongitude()); }convertLocationToCityId这种转换在课设里不需要做得太复杂用经纬度范围匹配附近城市就行。如果懒得做逆地理编码也可以简化成定位成功就更新一次城市名失败就用默认城市。4.2 请求天气接口报 Cleartext HTTP traffic not permitted现象App 跑起来了但 Logcat 里出现红色异常关键字是Cleartext HTTP traffic to api.weather.example.com not permitted界面上一片空白。原因Android 9.0API 28开始系统默认禁止应用使用明文 HTTP 流量。如果天气接口用的是 http:// 而不是 https://请求会被直接拦下来。解决在 Manifest 的 application 标签里打开明文流量开关。这个做法只影响调试阶段课设足够。application android:usesCleartextTraffictrue ...如果不想全局放开也可以只针对某个域名配置网络安全策略但课设场景没有必要全局开关最简单。要注意的是这个属性在application标签下不是manifest根节点下写错位置会直接编译报错。4.3 温度、天气现象全是 nullJSON 字段名和 Bean 不一致现象App 能正常运行列表也能滑但每一个 item 上面显示的都是 null温度那栏只有一个℃。原因Gson 是严格按字段名映射的。接口返回的是tempMax你 Bean 里写的是tempMax还好如果手滑写成temp_max或者TempMax解析结果就是 null。还有一种情况是嵌套层级不对接口返回的是data.now.temp你的 Bean 里没有data这一层直接把now放最外层了。解决先用浏览器打开接口地址把返回的 JSON 原文复制进任意能格式化 JSON 的编辑器里看清楚层级关系再对照着改 Bean。最快的排查方法是在解析后打印一行日志Log.d(WeatherBean, new Gson().toJson(bean));如果输出{code:200,now:null}说明是now那层的字段名或结构对不上先修 Bean 的嵌套结构再修字段名。这里花 10 分钟排查比反复改代码重跑省时间得多。4.4 首次运行直接闪退key 没填或者子线程更新了 UI现象安装成功点击图标应用闪一下就没。原因最常见的有两个。一是天气接口的 key 没填或填错了接口返回错误码代码没有做异常处理直接在onResponse里解析了一个异常 JSON抛 JSON 解析异常。二是忘记回调线程这件事在 OkHttp 的子线程里直接调了textView.setText()触发主线程限制崩溃。解决看 Logcat 的 FATAL EXCEPTION 那一行定位是哪一种。如果崩溃信息里有MainThread或Only the original thread就是线程问题把 UI 更新代码包进runOnUiThread。如果崩溃信息里有Gson或JSONException去检查 key 和接口返回。注意把 key 从代码里找出来确认一下很多人把YOUR_KEY这个占位符原封不动留到运行。4.5 列表卡片滑动掉帧快速滑动时卡顿现象列表能显示但上下滑动时明显卡顿快速滑甚至出现白屏。原因天气 item 布局嵌套太深——LinearLayout 套 RelativeLayout 再套 LinearLayout每个 item 里还实时做一些字符串拼接和日期格式化。RecyclerView 虽然复用 ViewHolder但每次复用时还是要跑一遍布局里的测量逻辑嵌套越深越卡。解决两个习惯。第一给 RecyclerView 加setHasFixedSize(true)已经在第 3 章写过第二item 布局尽量拍平外层一个 LinearLayout里面几个 TextView避免多重嵌套。另外一个常见误用是在onBindViewHolder里做new SimpleDateFormat(...).format(...)这种耗时操作把日期格式化放到 Bean 的 getter 里或者提前在解析阶段就格式化好不要在滚动回调里现算。5. 从能交差到满分课设四个加分项直接写进工程5.1 多城市管理与本地缓存离线演示不露怯课设老师最爱问的问题之一是这个 App 支持几个城市。如果你只有一个固定的默认城市回答会很尴尬。多城市管理用 SharedPreferences 就能做不需要引入数据库。private static final String PREF_CITIES pref_cities; private static final String KEY_CITIES key_cities; public static void saveCityList(Context context, ListString cityIds) { SharedPreferences sp context.getSharedPreferences(PREF_CITIES, Context.MODE_PRIVATE); android.text.TextUtils.join(,, cityIds); sp.edit().putString(KEY_CITIES, TextUtils.join(,, cityIds)).apply(); } public static ListString loadCityList(Context context) { SharedPreferences sp context.getSharedPreferences(PREF_CITIES, Context.MODE_PRIVATE); String raw sp.getString(KEY_CITIES, 101010100); return Arrays.asList(raw.split(,)); }apply()是异步写入不会阻塞主线程。用一个逗号分隔的字符串保存城市 ID 列表读取时再拆开足够支撑课设。缓存是另一个加分点。把每次请求成功的 JSON 原样存下来冷启动时先渲染缓存再发请求sp.edit().putString(last_json, json).apply();下次启动直接读last_json解析上屏。这个逻辑在答辩现场特别有用把网络断了打开 App天气数据还在城市还能切换老师会认为你考虑到了弱网和离线场景。5.2 下拉刷新与失败重试态界面不只是加载成功一种样子SwipeRefreshLayout 是官方下拉刷新组件用法很固定它包住 RecyclerView监听刷新事件后重新请求数据请求结束调用setRefreshing(false)收起刷新动画。SwipeRefreshLayout refreshLayout findViewById(R.id.swipe_refresh); refreshLayout.setOnRefreshListener(() - { fetchWeather(currentCityId); });另一个容易忽略的是请求失败时的界面。加载失败不能干瞪眼看着空白列表至少要有两种补救态一种是把上次缓存的数据继续显示顶部弹一个 Toast 提示数据已过期另一种是根本没有缓存显示一行加载失败点击重试的小布局。重试按钮绑定同一个fetchWeather方法代码量不大但界面完整度会高一大截。5.3 启动页、应用图标和命名规范答辩的第一印象很多课设项目没有启动页点击图标后直接进主界面。但完整的 App 应该有一个启动阶段。不推荐单独写一个 SplashActivity 然后 Handler 延迟跳转——那是在给自己制造麻烦。正确做法是用主题的windowBackgroundstyle nameTheme.Weather parentTheme.AppCompat.Light.NoActionBar item nameandroid:windowBackgrounddrawable/bg_splash/item /stylebg_splash是一张带 logo 和 App 名称的图片。应用启动时自动显示绘制完首帧进入 MainActivity 后自然被覆盖不需要额外写跳转代码也没有白屏闪烁的问题。应用图标直接替换mipmap目录下对应密度的图片不要用默认的机器人图标交差。还有命名规范类名首字母大写、资源文件小写下划线、包名改成自己的域名。老师打开工程扫一眼代码命名规整不规整印象分已经有了差距。6. 答辩前一天我用这三个方法自测App第一个方法验证定位链路。模拟器里打开 Extended Controls手动设置一组你所在位置的经纬度发送后打开 App 看城市是否变化。真机上更简单走到窗边打开 App 授权定位看城市名是否自动更新。这两个动作都通过定位这条链路才算真的通了。第二个方法验证请求和解析。开着抓包工具或者在 HttpUtil 的onResponse里打一行日志把返回的 JSON 打出来核对三个值code是不是 200、now.temp是不是正常温度、forecast数组有没有内容。我见过太多人界面显示正常但数据其实是写死在代码里的假数据这种在答辩时一改城市就露馅。所以要确保你看到的是真实接口返回值。第三个方法验证异常态。把手机开飞行模式再打开 App看它是否闪退。不闪退且能显示缓存数据或一个友好的失败提示才算过关。然后再关掉飞行模式点重试按钮看数据能不能恢复。这两个场景跑一遍比功能正常更让答辩老师放心。最后聊一个我自己的习惯每次提交课设前会把临时调试用的日志、写死的经纬度、多余的测试按钮全部清掉然后把 key 和默认城市 ID 提到常量类里。这些看不见的工作不会出现在功能演示里但能避免现场翻车。希望这篇文章能帮到你祝答辩顺利。本文还有配套的精品资源点击获取