简介:一套面向安卓开发初学者的天气预报应用完整工程,纯Java实现,不依赖第三方SDK,直接对接公开天气API(如OpenWeather等)完成城市查询、实时温度、天气状态图标和基础UI展示。项目已按标准Android Studio规范组织,包含app模块、src源码目录、res资源文件、proguard混淆配置、gradle构建脚本(含build.gradle、gradlew、settings.gradle)、.gitignore和.idea配置,支持一键导入、编译与真机/模拟器运行。代码手工编写,关键逻辑配有中文注释,便于理解请求流程、数据解析(JSON)、Activity生命周期管理及RecyclerView简单列表渲染。适合高校移动开发课程实践、期末作业参考或自学练手,无需额外环境配置即可上手修改调试。
1. 项目概述:为什么这个天气App源码值得你花30分钟认真看一遍
如果你正在上高校的Android开发课,或者刚学完Java基础、正琢磨着怎么把课本上的Activity、Intent、HTTP请求这些概念串成一个能真正在手机上跑起来的东西——那这个天气App源码包,就是我当年带学生做课程设计时反复打磨出的“第一块真实砖头”。它不是那种只有一两个空Activity、靠Toast弹个“Hello World”的教学Demo,也不是动不动就引入Retrofit+RxJava+MVVM三层架构、让新手一打开就头皮发麻的“进阶样板”。它就是一个用最朴素的Android原生组件、最直白的Java语法、最标准的Gradle组织方式,把“用户输入城市名→发起网络请求→解析JSON→更新UI”这条主链路,从头到尾给你拉得清清楚楚的工程。
关键词里提到的“Android天气”“Java源码”“Gradle项目”“天气App教学”,每一个都不是虚词。我特意没用Kotlin,因为很多高校课程大纲里Java仍是必修语言;没封装任何第三方网络库,所有HTTP请求都基于Android SDK自带的HttpURLConnection(配合AsyncTask做简单线程切换),就是为了让你一眼看清数据是怎么从服务器飞过来、又怎么被拆解成温度、湿度、天气图标这些具体字段的;Gradle配置也刻意保持极简——没有自定义插件、没有多模块嵌套、没有复杂的Maven仓库镜像配置,build.gradle里连compileSdkVersion和targetSdkVersion都写死在注释里,告诉你为什么选33而不是34,为什么minSdkVersion卡在21(即Android 5.0 Lollipop),这背后是兼容性与API能力的权衡,不是随便填的数字。
这个项目真正解决的,是一个初学者最常卡住的“断层感”:课堂上讲了JSON解析,但没人告诉你JSONObject和JSONArray嵌套五层时怎么一层层get()才不崩溃;讲了RecyclerView,但没演示过怎么把天气预报的7天列表,用一个ArrayList<WeatherDay>塞进去再交给Adapter;讲了Activity生命周期,但没结合实际场景说明为什么网络请求失败后,onResume()里要重新检查网络状态并提示用户重试。而这个源码包,每一处关键逻辑旁边都带着中文注释,比如在WeatherActivity.java第87行写着:“// 注意:此处必须在主线程更新UI,AsyncTask.onPostExecute()已保证在主线程执行,不可在此处再开新线程或调用runOnUiThread()”,这种细节,只有踩过坑的人才会写得这么笃定。
它适合谁?高校学生交期末作业时直接改城市名、换图标、加个“刷新按钮”就能交差;自学党想验证自己写的HTTP请求是否正确,把ApiService.java里的URL替换成自己的OpenWeather API Key,5分钟就能看到真机上跳出“北京:22℃,晴”,建立正向反馈;甚至刚转行的初级开发者,拿它当“结构模板”,把里面的app/src/main/res/layout/activity_weather.xml复制过去,再把WeatherModel.java的字段换成自己业务的数据结构,就能快速搭起一个新项目的骨架。它不炫技,但每一步都踩在初学者最容易滑倒的地方,扶你站稳。
2. 整体架构与设计思路:为什么不用Retrofit、不用Kotlin、甚至不用OkHttp?
2.1 核心设计原则:教学优先,可追溯性至上
这个项目的架构图,如果画出来,其实就一张A4纸大小:WeatherActivity(主界面)→ ApiService(网络请求类)→ WeatherParser(JSON解析器)→ WeatherModel(数据模型)→ WeatherAdapter(列表适配器)。没有Repository,没有ViewModel,没有DataBinding,甚至连BaseActivity这种抽象父类都没有。原因很简单:教学场景下,抽象层级越多,学生越难定位问题根源。我见过太多学生,在Retrofit的Call<T>回调里加断点,结果发现断点根本进不去,最后折腾半天才发现是@GET注解里的URL少了个斜杠,或者ConverterFactory没配对。而在这个项目里,所有网络请求逻辑都在ApiService.java的fetchWeatherByCityName()方法里,12行代码,从创建连接、设置超时、发送请求、读取响应流、到关闭连接,一行行跟着走,错误堆栈直接指向哪一行,改起来毫不费力。
提示:
ApiService.java中HttpURLConnection的setConnectTimeout(10000)和setReadTimeout(10000)设为10秒,这是经过实测的平衡点——太短(如3秒)容易因校园网波动误判为超时;太长(如30秒)会让用户觉得App“卡死”。你在调试时可以临时改成5000,模拟弱网环境测试重试逻辑。
2.2 Gradle构建体系:为什么保留gradlew.bat和gradlew双脚本?
目录里同时存在gradlew.bat(Windows批处理)和gradlew(Linux/macOS Shell脚本),这不是冗余,而是教学必需。很多学生第一次在实验室Windows电脑上导入项目,双击gradlew.bat报错“找不到Java环境”,这时老师就能顺势讲清楚JDK环境变量JAVA_HOME怎么配;而当他回家用Mac,发现./gradlew执行不了,提示“Permission denied”,正好带他理解Unix文件权限chmod +x gradlew的意义。gradle/wrapper/gradle-wrapper.properties里明确写着distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip,选7.5而非最新的8.x,是因为Android Studio Giraffe(当前高校主流版本)对Gradle 7.5兼容性最稳定,升级到8.x后build.gradle里implementation写法虽不变,但android.useAndroidX=true等迁移配置会触发额外警告,徒增干扰。
build.gradle(Project级)里最关键的两行是:
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2' // 对应AGP版本,必须与Gradle 7.5匹配
}
}
和
allprojects {
repositories {
google() // 必须放在jcenter()之前,否则部分依赖下载失败
mavenCentral()
}
}
这里有个易错点:google()仓库必须置于mavenCentral()之前。因为AndroidX组件(如androidx.appcompat:appcompat)优先从Google Maven获取,若顺序颠倒,Gradle可能去Maven Central找旧版支持库,导致NoClassDefFoundError。我在带学生调试时,有三分之一的问题都出在这两行顺序上。
2.3 资源与配置的“教科书式”组织
app/src/main/res/目录下,资源分类严格遵循Android官方规范:drawable/放天气图标(ic_sunny.png, ic_rainy.png),layout/放界面布局(activity_weather.xml, item_weather_day.xml),values/放字符串和颜色(strings.xml, colors.xml)。特别值得注意的是strings.xml里这行:
<string name="api_key_placeholder">YOUR_OPENWEATHER_API_KEY</string>
它没写成<string name="api_key">xxx</string>,而是用_placeholder后缀,就是为了强制你在首次运行前,必须手动打开ApiService.java,找到private static final String API_KEY = getString(R.string.api_key_placeholder);这一行,把getString(...)替换成硬编码的Key,或者更规范地——在gradle.properties里添加OPENWEATHER_API_KEY=your_key_here,再在build.gradle里通过buildConfigField注入。这个“必须动手改”的设计,比任何PPT讲解都更能让你记住“API Key不能硬编码在代码里”的安全铁律。
.gitignore文件也经过精心裁剪,除了标准的.gradle/, /build/, .idea/,还特意加了:
# 教学提示:以下文件请勿提交至Git,应在本地配置
local.properties
app/src/main/res/values/api_keys.xml
这就是在暗示你:local.properties里存SDK路径,api_keys.xml里存敏感信息,它们属于“本地环境配置”,和项目逻辑无关,不该进版本库。这种通过.gitignore传递工程规范的方式,比口头强调十遍都管用。
3. 核心功能实现详解:从输入城市到UI刷新的完整链路
3.1 城市查询入口:SearchView的轻量级集成
主界面activity_weather.xml顶部的搜索栏,用的是原生SearchView而非第三方控件。它的初始化在WeatherActivity.java的onCreate()里:
SearchView searchView = findViewById(R.id.search_view);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQuerySubmit(String query) {
if (!query.trim().isEmpty()) {
fetchWeather(query.trim()); // 关键:触发请求
}
return true;
}
// ... onQueryTextSubmit省略
});
这里有两个教学重点:第一,onQuerySubmit()里必须对query做trim(),否则用户手抖多按个空格,请求就会变成https://api.openweathermap.org/data/2.5/weather?q= Beijing&appid=xxx,OpenWeather API会返回city not found;第二,return true表示“我已经处理了这个提交事件,不需要系统再做默认行为”,这是SearchView的契约,漏写会导致键盘不收起。
注意:
SearchView的android:iconifiedByDefault="false"属性在XML里设为false,是为了让学生一进入界面就看到搜索框处于展开状态,降低操作门槛。实际产品中通常设为true,用放大镜图标触发。
3.2 网络请求与JSON解析:HttpURLConnection + JSONObject的手工拆解
ApiService.java是整个数据链路的心脏。以获取当前天气为例,核心方法fetchCurrentWeather(String cityName)的流程如下:
-
拼接URL:
String urlStr = "https://api.openweathermap.org/data/2.5/weather?q=" + URLEncoder.encode(cityName, "UTF-8") + "&appid=" + API_KEY + "&units=metric";
这里URLEncoder.encode()必不可少。学生常犯的错误是直接拼接"q=" + cityName,当城市名含中文(如“上海”)时,URL变成q=上海,服务器无法识别。URLEncoder会将其转为q=%E4%B8%8A%E6%B5%B7,这才是合法的。 -
建立连接与读取响应:
java HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); int responseCode = conn.getResponseCode(); // 关键!必须先调用此方法触发请求 if (responseCode == HttpURLConnection.HTTP_OK) { InputStream is = conn.getInputStream(); String response = readStreamAsString(is); // 自定义工具方法,将流转为String return parseCurrentWeatherJson(response); // 解析JSON }
重点在于conn.getResponseCode()这行。很多初学者以为openConnection()就等于发出了请求,其实不然——它只是建立了连接通道,真正的HTTP请求是在调用getResponseCode()或getInputStream()时才发出的。漏掉这步,response永远是空。 -
JSON解析的“防崩”写法:
OpenWeather返回的JSON结构是:
json { "name": "Beijing", "main": {"temp": 22.5, "humidity": 65}, "weather": [{"main": "Clear", "description": "clear sky"}], "sys": {"country": "CN"} }
解析时,WeatherParser.java采用“层层防御”策略:
java try { JSONObject root = new JSONObject(jsonString); String cityName = root.optString("name", "Unknown"); // optString防null JSONObject mainObj = root.optJSONObject("main"); double temp = mainObj != null ? mainObj.optDouble("temp", 0.0) : 0.0; JSONArray weatherArr = root.optJSONArray("weather"); String weatherDesc = "N/A"; if (weatherArr != null && weatherArr.length() > 0) { JSONObject firstWeather = weatherArr.optJSONObject(0); weatherDesc = firstWeather != null ? firstWeather.optString("main", "N/A") : "N/A"; } return new WeatherModel(cityName, temp, weatherDesc); } catch (JSONException e) { Log.e("WeatherParser", "JSON parse error", e); return null; // 返回null,由上层判断并提示用户 }
全程使用optXXX()而非getXXX(),因为后者遇到缺失字段会抛JSONException导致App崩溃,而optXXX()在字段不存在时返回默认值(如optString("name", "Unknown")返回”Unknown”),把错误处理权交给业务层,这才是健壮应用的写法。
3.3 UI渲染:RecyclerView的极简实践与图标映射
天气预报列表用RecyclerView展示7天数据,其Adapter WeatherAdapter.java只有63行代码,却覆盖了初学者最需要的全部要点:
- ViewHolder复用机制:
onCreateViewHolder()只inflate一次item_weather_day.xml,onBindViewHolder()里只做数据填充,不创建新View; - 图标动态加载:根据天气描述字符串(如”Rain”, “Clouds”)映射到
drawable资源ID:
java private int getWeatherIconId(String weatherMain) { switch (weatherMain.toLowerCase()) { case "clear": return R.drawable.ic_sunny; case "rain": return R.drawable.ic_rainy; case "clouds": return R.drawable.ic_cloudy; case "snow": return R.drawable.ic_snowy; default: return R.drawable.ic_unknown; } }
这种硬编码映射,比用HashMap<String, Integer>更直观,学生一眼就能看懂逻辑流向; - 温度单位切换:
WeatherModel里temp字段单位是摄氏度(&units=metric),但UI显示时做了格式化:String.format("%.1f℃", model.getTemp()),%.1f确保小数点后一位,避免显示22.5000000001℃这种诡异数字。
主界面WeatherActivity的updateUI()方法,是UI更新的总开关:
private void updateUI(WeatherModel current, List<WeatherDay> forecastList) {
// 更新当前天气
TextView tvCity = findViewById(R.id.tv_city_name);
tvCity.setText(current.getCityName());
TextView tvTemp = findViewById(R.id.tv_current_temp);
tvTemp.setText(String.format("%.1f℃", current.getTemp()));
ImageView ivWeather = findViewById(R.id.iv_weather_icon);
ivWeather.setImageResource(getWeatherIconId(current.getWeatherDesc()));
// 更新7天预报列表
RecyclerView rvForecast = findViewById(R.id.rv_forecast);
WeatherAdapter adapter = new WeatherAdapter(forecastList);
rvForecast.setAdapter(adapter);
}
这里没有用LiveData或DataBinding,所有findViewById()都是手动调用,目的就是让你清晰看到“数据→View ID→View对象→属性设置”的完整链条。当你未来学ViewBinding时,会立刻明白它省掉的是哪几行重复代码。
4. 实操部署与调试指南:从零开始跑通真机的完整步骤
4.1 获取OpenWeather API Key的实操避坑指南
项目摘要里说“对接公开天气API”,但没说怎么获取Key。这里补全最易卡壳的三步:
- 访问 https://home.openweathermap.org/users/sign_up,用邮箱注册(无需付费,免费版限1000次/天);
- 登录后点击右上角头像 →
API keys→ 复制Default key那一串字符(形如a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6); - 关键操作:打开项目根目录下的
gradle.properties文件,添加一行:
OPENWEATHER_API_KEY=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
然后打开app/build.gradle,在android { }块内找到buildTypes,修改debug和release的buildConfigField:
gradle buildTypes { debug { buildConfigField "String", "API_KEY", "\"${OPENWEATHER_API_KEY}\"" } release { buildConfigField "String", "API_KEY", "\"${OPENWEATHER_API_KEY}\"" // ... 其他配置 } }
最后,在ApiService.java里,把原来的private static final String API_KEY = "YOUR_KEY_HERE";删掉,改为:
java private static final String API_KEY = BuildConfig.API_KEY;
这样,API Key就通过Gradle注入到了编译后的BuildConfig类中,既安全(不硬编码在源码里),又方便(改gradle.properties即可切换环境)。
注意:如果跳过第3步,直接在
ApiService.java里写死Key,虽然能跑通,但一旦提交到GitHub,你的Key就暴露了,会被恶意刷流量导致账号被封。我带过的班级里,有3个学生因此被OpenWeather邮件警告。
4.2 Android Studio导入与真机调试全流程
Step 1:确认环境
- Android Studio版本:建议Bumblebee(2021.1.1)或更高,但不要最新版(如Iguana),因新版本可能默认启用composeCompiler插件,与本项目Gradle 7.5冲突;
- JDK:必须是JDK 11(Android Studio自带),若系统装了JDK 17,需在File → Project Structure → SDK Location里手动指定JDK 11路径。
Step 2:导入项目
- 启动Android Studio → Open an existing project → 选择解压后的项目根目录(含settings.gradle的文件夹);
- 首次导入会触发Gradle同步,等待右下角“Gradle sync completed”提示。若报错Could not find method android() for arguments [...],说明build.gradle(Project级)里buildscript块位置错了——它必须在plugins块之前,且apply from:语句不能出现在buildscript内部。
Step 3:连接真机
- 手机开启“开发者选项”(连续点击“关于手机”里“版本号”7次);
- 开启“USB调试”,用数据线连接电脑;
- Android Studio右侧Device Selector里应出现设备名(如SM-G9730),若显示??????????,需安装对应手机厂商的USB驱动(三星用Smart Switch,华为用HiSuite);
- 点击绿色三角形Run app,AS会自动安装APK并启动WeatherActivity。
Step 4:调试网络请求
- 在ApiService.java的fetchCurrentWeather()方法开头加断点;
- 点击Debug app(虫子图标),输入城市名(如“Shanghai”)并提交;
- 当执行到conn.getResponseCode()时,观察Variables窗口里的urlStr,复制出来在浏览器访问,确认返回的是有效JSON;
- 若返回{"cod":"404","message":"city not found"},检查URL里城市名是否被正确URL编码(中文应为%E4%B8%8A%E6%B5%B7);
- 若返回{"cod":"401","message":"Invalid API key"},检查BuildConfig.API_KEY是否为空——此时回到gradle.properties确认Key是否粘贴完整,且无多余空格。
4.3 模拟器配置优化:让AVD跑得更快
若用模拟器(AVD),强烈建议按此配置避免卡顿:
- 系统镜像:选择
x86_64架构的Android 12.0 (S),而非ARM镜像(ARM模拟慢3倍以上); - 硬件加速:在
Tools → SDK Manager → SDK Tools里,勾选Intel x86 Emulator Accelerator (HAXM installer)并安装; - AVD设置:内存设为2048MB,VM Heap设为256MB,Graphics选
Hardware - GLES 2.0; - 启动参数:在AVD编辑页的
Show Advanced Settings → Boot Options → Additional Command Line Options里,填入:
-gpu swiftshader_indirect -no-window -no-audio
swiftshader_indirect启用软件渲染加速,-no-window后台运行(节省桌面空间),-no-audio禁用音频(减少CPU占用)。
实测下来,这样配置的AVD启动时间从2分钟缩短到20秒,RecyclerView滑动帧率稳定在55fps以上,完全满足教学演示需求。
5. 常见问题与排查技巧实录:那些我没写在注释里的血泪经验
5.1 网络请求失败的四大高频原因及速查表
| 现象 | 可能原因 | 排查命令/操作 | 解决方案 |
|---|---|---|---|
App启动后空白,Logcat显示java.net.UnknownHostException: api.openweathermap.org | DNS解析失败 | 在手机浏览器访问http://api.openweathermap.org,看能否打开 | 检查手机网络是否正常;若用校园网,尝试切换WiFi或移动数据;在ApiService.java的URL里把https换成http(OpenWeather支持HTTP) |
| 输入城市后无反应,Logcat无网络相关日志 | SearchView未正确绑定 | 在WeatherActivity.java的onCreate()里,检查findViewById(R.id.search_view)是否返回null | 确认activity_weather.xml中SearchView的android:id="@+id/search_view"拼写正确;检查setContentView(R.layout.activity_weather)是否在findViewById之前调用 |
请求返回{"cod":"400","message":"Nothing to geocode"} | 城市名为空或纯空格 | 在onQuerySubmit()里加Log.d("Search", "Query: '" + query + "'"); | 在fetchWeather()方法开头加if (query == null || query.trim().isEmpty()) { Toast.makeText(this, "请输入城市名", Toast.LENGTH_SHORT).show(); return; } |
| 真机上能运行,模拟器上闪退 | 模拟器缺少Google Play服务 | 在AVD管理器里,创建AVD时选择Target为Google APIs而非Android TV | 重新创建AVD,System Image选x86_64 + Google APIs;或改用物理机调试 |
实操心得:我在实验室带学生时,80%的“网络请求失败”问题,都能通过在
ApiService.java的fetchCurrentWeather()方法第一行加Log.d("ApiService", "Requesting URL: " + urlStr);快速定位。URL打印出来,复制到浏览器一试,问题立现——是网络问题、Key问题,还是拼写问题,一目了然。
5.2 UI显示异常的典型场景与修复
场景1:天气图标不显示,只显示空白方块
- 原因:getWeatherIconId()方法里,weatherMain字符串包含空格或大小写不匹配(如OpenWeather返回"Clear",但代码里写的是"clear");
- 修复:在switch语句前加日志Log.d("WeatherIcon", "Raw weather: " + weatherMain);,观察实际返回值;将switch条件统一转为小写:weatherMain.toLowerCase().equals("clear")。
场景2:7天预报列表只显示第一项,其余为空
- 原因:WeatherAdapter.java的getItemCount()方法返回了错误的size,比如写成了return 1;;
- 修复:检查getItemCount()是否正确返回mWeatherDays.size();在onBindViewHolder()里加Log.d("Adapter", "Binding position: " + position + ", size: " + getItemCount());,确认position不会越界。
场景3:输入城市后,当前温度显示0.0℃,但网络请求返回的JSON里temp字段是正确的
- 原因:WeatherParser.java里解析temp时用了getInt()而非getDouble(),导致小数部分被截断;
- 修复:将root.getJSONObject("main").getInt("temp")改为root.getJSONObject("main").getDouble("temp");更稳妥的做法是用optDouble("temp", 0.0),并确保WeatherModel的temp字段是double类型而非int。
5.3 Gradle构建失败的“三板斧”解决方案
当Android Studio右下角弹出Gradle build failed,别急着重装AS,先按顺序执行这三步:
第一板斧:清理构建缓存
- Build → Clean Project
- Build → Rebuild Project
- 若仍失败,在终端(Terminal)里执行:
bash ./gradlew clean ./gradlew build --stacktrace
--stacktrace会输出详细错误路径,比AS界面提示更精准。
第二板斧:检查Gradle与AGP版本匹配
- 查阅Android Gradle Plugin 版本对应表,确认gradle-wrapper.properties里的Gradle版本(如7.5)与build.gradle(Project级)里的AGP版本(如7.4.2)兼容。不匹配时,AS会报Minimum supported Gradle version is X.X.X. Current version is Y.Y.Y,此时只需修改gradle-wrapper.properties里的distributionUrl为对应版本即可。
第三板斧:重置IDE配置
- 关闭AS → 删除项目根目录下的.idea/文件夹和*.iml文件 → 重启AS重新导入项目。
这招专治“AS缓存了错误的模块依赖关系”,比如明明app模块依赖androidx.appcompat,却报Cannot resolve symbol AppCompatActivity。删除.idea后,AS会重新扫描build.gradle,重建正确的索引。
最后分享一个小技巧:在
app/src/main/AndroidManifest.xml里,<application>标签下加一行android:usesCleartextTraffic="true"。这是因为OpenWeather的HTTP接口(非HTTPS)在Android 9.0+默认被禁止,加这行后,HTTP请求才能通过。虽然OpenWeather推荐用HTTPS,但教学场景下,用HTTP能避免学生被SSL证书问题绕晕——毕竟,教会他们理解“网络请求”本身,比理解“TLS握手”更重要。
6. 教学延展与二次开发建议:如何把这个项目变成你的个人作品集亮点
这个天气App绝不仅是个“交作业的玩具”。只要稍作延展,它就能成为你求职简历上“独立开发Android应用”的有力证明。以下是三个低门槛、高价值的改造方向,我都带学生实操过,效果显著:
6.1 加入定位功能:从“输入城市”到“自动定位”
OpenWeather提供经纬度查询接口:https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={key}&units=metric。改造步骤:
- 在
AndroidManifest.xml里添加定位权限:
xml <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> - 在
WeatherActivity.java的onCreate()里,调用LocationManager获取最后一次已知位置:
java LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { Location lastKnown = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (lastKnown != null) { double lat = lastKnown.getLatitude(); double lon = lastKnown.getLongitude(); fetchWeatherByLatLon(lat, lon); // 新增方法 } } - 在
ApiService.java里新增fetchWeatherByLatLon(double lat, double lon)方法,拼接对应URL。
教学价值:学生立刻理解“权限申请→位置获取→接口调用”的完整链路,比单纯学Activity生命周期生动得多。我带的一个学生,就靠这个功能拿了校级创新大赛二等奖。
6.2 实现夜间模式:用AppCompatDelegate实践Material Design
Android 10+原生支持深色主题,但教学项目需兼容低版本。方案是用AppCompatDelegate:
- 在
WeatherActivity.java的onCreate()里,super.onCreate()之前加:
java AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - 在
res/下新建values-night/colors.xml,定义深色背景色:
xml <color name="background_color">#121212</color> <color name="text_primary">#FFFFFF</color> - 在
activity_weather.xml里,将android:background属性改为@color/background_color。
效果:当用户在系统设置里切换深色模式,App立即响应,UI文字、背景色自动适配。这让学生直观感受到“资源限定符(values-night)”的威力,远胜于背诵概念。
6.3 添加离线缓存:用SharedPreferences保存最近查询
避免每次启动都请求网络,提升用户体验:
- 在
WeatherActivity.java里,定义SharedPreferences:
java private SharedPreferences mPrefs; private static final String PREFS_NAME = "weather_prefs"; private static final String KEY_LAST_CITY = "last_city"; private static final String KEY_LAST_TEMP = "last_temp"; - 在
onCreate()里初始化:
java mPrefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); String lastCity = mPrefs.getString(KEY_LAST_CITY, ""); if (!lastCity.isEmpty()) { // 显示上次城市和温度 updateUIFromCache(lastCity, mPrefs.getFloat(KEY_LAST_TEMP, 0f)); } - 在
updateUI()成功后,保存到缓存:
java SharedPreferences.Editor editor = mPrefs.edit(); editor.putString(KEY_LAST_CITY, current.getCityName()); editor.putFloat(KEY_LAST_TEMP, (float) current.getTemp()); editor.apply();
教学意义:学生亲手实现了“数据持久化”的最小闭环,理解SharedPreferences的apply()(异步)与commit()(同步)区别,为后续学SQLite打下直觉基础。
这三个延展,每个都能在2小时内完成,却能让项目从“教学Demo”跃升为“有思考、有用户意识”的作品。记住,技术深度不在于用了多少框架,而在于你是否真正理解每一行代码在解决什么问题、为何这样解决——而这,正是这个天气App源码想传递给你的,最核心的东西。
简介:一套面向安卓开发初学者的天气预报应用完整工程,纯Java实现,不依赖第三方SDK,直接对接公开天气API(如OpenWeather等)完成城市查询、实时温度、天气状态图标和基础UI展示。项目已按标准Android Studio规范组织,包含app模块、src源码目录、res资源文件、proguard混淆配置、gradle构建脚本(含build.gradle、gradlew、settings.gradle)、.gitignore和.idea配置,支持一键导入、编译与真机/模拟器运行。代码手工编写,关键逻辑配有中文注释,便于理解请求流程、数据解析(JSON)、Activity生命周期管理及RecyclerView简单列表渲染。适合高校移动开发课程实践、期末作业参考或自学练手,无需额外环境配置即可上手修改调试。


被折叠的 条评论
为什么被折叠?



