
简介一份基于 Android 的天气预报课程设计报告从移动应用开发的完整流程出发围绕城市选择、实时天气获取、风向温度显示等核心功能展开内容覆盖 Android 系统概述、需求分析、系统流程图、界面与数据库设计、网络连接方案以及功能测试与评估方法适合高校计算机或电子类专业学生完成移动开发课程作业、课程设计或毕业设计时参考。压缩包内共 1 个 doc 文档大小 609KB文档包含摘要、目录、项目评分表和各章节实施细节虽是单文件但结构完整可直接作为设计报告模板使用。该资源在 CSDN 上已有 290 人次浏览学习结合预览中的欢迎界面、菜单界面、主工程模块设计和模拟器/真机测试等内容能帮助初学者快速把握 Android 应用开发从规划到落地的关键节点减少从零摸索的时间成本。1. 基于Android的天气预报开发先想清楚课程设计要交什么能跑通的天气Demo一个周末就能写出来但如果这是你的课程设计答辩时老师大概率不会满足于“打开App能看到温度”。我见过不少项目挂在同一个地方网络请求明明返回了JSON也解析了但界面卡死、切后台回来进度条不消失、没网时白屏、清掉缓存重装又得重新选城市。基于Android的天气预报开发这个题目真正的分水岭不在于写出多少行代码而在于你是否把异常路径和生命周期当成代码的一部分。这篇文章会按理论、实现、产品化、交付的顺序把一套可以直接拿去答辩的完整方案和参数讲清楚。2. 天气预报App的Android技术选型网络库、解析器与状态管理2.1 为什么选OkHttp Gson而不是HttpURLConnection org.json写课程设计时最常见的误区是只依赖Android自带SDK里的API。HttpURLConnection不是不能用但你需要手动管理线程、手动拼接URL、手动解析JSON代码最后全部堆在Activity里稍微一改就崩。OkHttp虽然是第三方库但它在连接复用、超时控制、请求取消上都比自带API可靠得多。用OkHttp发请求用Gson把响应体直接映射成数据类是Android开发里最省心智的一条路。从这个项目一开始我就把依赖范围限制在最小集OkHttp负责网络传输Gson负责JSON绑定协程负责异步切换。三者各管一段遇到问题能快速定位在网络层还是解析层。需要说明的是不要为了追求“新技术”直接引入Retrofit。Retrofit本身是对OkHttp的进一步封装课程设计里可以用但它会掩盖掉HTTP请求的底层细节答辩时老师问你“请求头在哪加的”反而说不清。OkHttp Gson这个组合正好处在“够用”和“能讲”的临界点上。中间层有一个容易被忽略的选择数据实体要不要和JSON字段完全对齐。大多数天气API返回的是嵌套结构比如外层是code和updateTime内层是now和daily。我的做法是准备两层模型网络层DTO和UI层Model。DTO负责忠实接收接口字段UI层住在Activity里手动映射这样如果换了天气API只需要改DTO和映射函数不会把接口变化传染给整个布局代码。2.2 天气JSON的实体映射字段命名和嵌套是第一个坑天气API返回的JSON里字段命名风格通常是驼峰或下划线而且嵌套层级很深。以常见的免费天气接口为例响应体往往长这样{ code: 200, updateTime: 2025-01-01T12:00:0008:00, now: { temp: 23, text: 多云, windDir: 东北风 } }对应的Kotlin数据类要写成这样data class WeatherResponse( SerializedName(code) val code: String, SerializedName(updateTime) val updateTime: String, SerializedName(now) val now: Now ) { data class Now( SerializedName(temp) val temp: String, SerializedName(text) val text: String, SerializedName(windDir) val windDir: String ) }这里有两个值得注意的细节。第一updateTime是带时区的字符串不要直接转Date课程设计里尽量原样保存展示时再做格式化。第二temp字段在接口里是字符串23不是整数23。很多同学一看到温度就下意识写成Int结果Gson解析直接抛JsonSyntaxException整个App崩溃。所以实体类的字段类型必须和接口实际返回保持一致需要计算时再toInt或toFloat。数据类开发完成后用一段测试代码验证字段映射是否成功把上面的JSON字符串喂给Gson正常解析出一个WeatherResponse对象断言的now.temp等于23。这一步放在后面第5章的单测里现在先记住结论。2.3 进度条和加载状态不要只在有数据时才想起来更新UI课程设计里最常见的一个现象是网络没返回时界面上没有任何反馈返回后直接setText。这不算功能错误但演示时如果网速慢评委看到的就是一个“卡住的白屏”。所以我一般会在Activity里维护一个简单的加载状态Loading、Success、Error。界面根据状态切换进度条、内容区和错误提示。这里的进度条不是百分比进度而是加载动画。Android里的ProgressBar配合Visibility控制是最基础的做法。注意不要把进度条和内容区叠在同一个位置我用ConstraintLayout时习惯把进度条放在温度文字区域加载时把温度隐藏、进度条显示数据到达后再反过来切换。这种状态机虽然简单但能把异步过程变成确定性流程后期排查问题非常有效。3. 在Android Studio里从零跑通天气预报的最小实现3.1 项目初始化与AndroidManifest配置要点用Android Studio新建一个Empty Activity项目包名按你的课程设计编号来取就行。要特别注意SDK版本问题Android 6.0以上网络权限属于普通权限只需要在Manifest里声明不需要动态申请但漏掉这条会导致请求时抛UnknownHostException很多人第一反应是API写错实际是权限没有声明。我习惯一上来把权限写齐网络请求、网络状态、粗定位、精定位。定位权限后面会用到如果不在Manifest里提前声明运行时再补会比较手忙脚乱。uses-permission android:nameandroid.permission.INTERNET/ uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE/ uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/还有一个非常隐蔽的坑Android 9.0开始默认禁止明文HTTP流量。如果你接入的天气API是http://而不是https://需要在application标签里加上android:usesCleartextTraffictrue。这一条几乎每个天气课程设计里都会出现直接把App从“连不上网”变“一键复活”。3.2 主界面布局把天气卡片和加载进度条放进同一套约束里布局我用ConstraintLayout写结构很简单顶部显示城市名称中间大号字体显示温度温度下方一行显示天气现象和风力底部放一个ProgressBar加载动画。ProgressBar默认是gone只有发起请求时才设为visible。androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightmatch_parent TextView android:idid/tv_city android:layout_widthwrap_content android:layout_heightwrap_content android:textSize20sp app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent/ TextView android:idid/tv_temp android:layout_widthwrap_content android:layout_heightwrap_content android:textSize56sp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent/ ProgressBar android:idid/loading style?android:attr/progressBarStyleLarge android:layout_widthwrap_content android:layout_heightwrap_content android:visibilitygone app:layout_constraintTop_toTopOfid/tv_temp app:layout_constraintBottom_toBottomOfid/tv_temp app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent/ /androidx.constraintlayout.widget.ConstraintLayout注意这里的ProgressBar用style?android:attr/progressBarStyleLarge定义了系统自带的大号轮圈样式不需要额外引入主题。温度TextView和进度条用一组约束居中加载时温度隐藏、进度条出现画面不会上下跳动。我一般在监听数据返回的入口调用binding.loading.visibility View.VISIBLE在成功或失败回调里统一置为GONE避免忘记关闭。3.3 用协程把网络请求和UI更新写在同一段代码里课程设计里最忌讳的是把网络请求直接写在onCreate里然后通过ThreadHandler切回主线程。现在Android官方已经把AsyncTask标记为废弃正确做法是用协程。在build.gradle里加上lifecycle-runtime-ktx依赖之后Activity内可以直接使用lifecycleScope启动一个协程这样协程的生命周期会跟随Activity页面销毁时自动取消请求不会泄漏。class MainActivity : AppCompatActivity() { private val client OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) loadWeather(101010100) } private fun loadWeather(cityId: String) { binding.loading.visibility View.VISIBLE lifecycleScope.launch(Dispatchers.IO) { val request Request.Builder() .url(https://api.example.com/weather?cityid$cityId) .build() runCatching { client.newCall(request).execute().use { response - val body response.body?.string() ?: returnlaunch Gson().fromJson(body, WeatherResponse::class.java) } }.onSuccess { weather - withContext(Dispatchers.Main) { bindWeather(weather) } }.onFailure { e - withContext(Dispatchers.Main) { binding.loading.visibility View.GONE binding.tvTemp.text 加载失败 } } } } private fun bindWeather(weather: WeatherResponse) { binding.loading.visibility View.GONE binding.tvCity.text weather.now.text binding.tvTemp.text weather.now.temp ℃ } }代码里的cityId是天气API要求传的城市行政代码不是城市名。101010100是北京的编号不同API对应关系不同一定要以你接入的接口文档为准。这里用execute()而不是enqueue()是因为协程已经把整个请求切到IO线程execute()是同步调用顺序写下来比回调更直观。runCatching负责捕获异常包括DNS解析失败、超时、JSON转换崩溃全部统一走onFailure。还有一个参数要留意OkHttpClient没有显式设置超时的话默认连接超时10秒、读取超时10秒。模拟器环境下DNS解析慢10秒可能不够所以我在构造客户端时把读超时也设成10秒避免请求卡在半路。真机上4G/5G网络一般没问题但如果是校园网环境建议调大到15秒同时把失败提示做成可重试按钮。4. 让课程设计更像产品城市定位、下拉刷新与缓存4.1 用系统定位拿到城市再映射成天气API的城市ID很多天气API按城市ID请求而不是直接传经纬度。所以要先把定位结果转换成城市名再映射到城市表。Android系统自带LocationManager和Geocoder不需要额外SDK只要在Manifest里声明了定位权限就能用。private fun locateCity() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_LOCATION ) return } val locationManager getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)?.let { location - lifecycleScope.launch(Dispatchers.IO) { val geocoder Geocoder(thisMainActivity, Locale.getDefault()) val addresses geocoder.getFromLocation(location.latitude, location.longitude, 1) withContext(Dispatchers.Main) { val city addresses?.firstOrNull()?.locality ?: 北京 binding.tvCity.text city } } } }注意getLastKnownLocation可能返回空也就是设备从未定位到当前位置。我在代码里用Elvis运算符兜底成“北京”保证界面不会出现空白城市名。课程设计里这个兜底逻辑非常关键因为它保证了你答辩演示时无论有没有信号App都能正常展示。定位本身是耗时操作所以放在Dispatchers.IO线程拿到结果后再切回主线程更新UI。4.2 下拉刷新不要挡住进度条SwipeRefreshLayout和ProgressBar的联动如果同时使用SwipeRefreshLayout和ProgressBar会出现两个转圈动画叠在一起的现象。我建议把两者当成互斥状态首次进入或切换城市时用页面中间的ProgressBar用户主动下拉刷新时用SwipeRefreshLayout的转圈。每次加载前先取消另一个状态代码就不容易乱。binding.swipeRefresh.setOnRefreshListener { binding.loading.visibility View.GONE binding.swipeRefresh.isRefreshing true loadWeather(currentCityId) } private fun bindWeather(weather: WeatherResponse) { binding.loading.visibility View.GONE binding.swipeRefresh.isRefreshing false // ... 更新UI }SwipeRefreshLayout的isRefreshing属性需要在网络回调里手动关闭。很多课程设计里会出现一个bug刷新动画转个不停原因是只有成功分支里取消了刷新进度失败分支直接return。统一在onSuccess和onFailure里都执行置false才能保证动画结束。刷新时还应该做一个防抖处理如果上一次请求还没结束再点下拉刷新直接忽略。4.3 用SharedPreferences做离线缓存存JSON和时间戳天气预报的数据是时敏的但不需要每次打开都重新请求。用SharedPreferences存一条JSON和一条时间戳读取时判断时间差比如20分钟以内直接用缓存超过20分钟再请求。这样既能加快第二次启动速度也能在断网时让App仍然显示上次天气。private fun saveCache(json: String) { getSharedPreferences(weather_cache, MODE_PRIVATE) .edit() .putString(json, json) .putLong(time, System.currentTimeMillis()) .apply() } private fun loadCache(): PairString, Long? { val sp getSharedPreferences(weather_cache, MODE_PRIVATE) val json sp.getString(json, null) ?: return null val time sp.getLong(time, 0L) return json to time }使用时先读缓存如果缓存时间未过期直接解析缓存并展示过期后先展示缓存再在后台请求新数据等新数据到达后替换UI并更新缓存。这种“陈旧数据先展示”的策略在课程设计报告里是加分项因为它体现了用户体验思维。注意PairString, Long返回的是Kotlin内置二元组不要自己写一个数据类这样简洁得多。4.4 排错表课程设计里最常见的5个运行时报错运行到一半崩溃或者没反应最影响答辩心态。我整理了一份高频排错表遇到对应现象直接按表检查。现象可能原因处理办法进度条一直转圈网络权限未声明、API Key错误、明文HTTP被拦截检查Manifest权限看Logcat里的具体Exception点击刷新闪退JSON字段类型不匹配Gson抛JsonSyntaxException把接口返回的原始JSON存到Log里对比数据类字段定位到城市但天气没变城市ID映射表不完整定位城市没有对应编号在城市映射表里加默认值未匹配统一返回“北京”切后台回来进度条还在转协程没有取消实际是Activity已销毁但请求还在使用lifecycleScope替代自定义CoroutineScope模拟器无法访问宿主机API模拟器里要用10.0.2.2代替127.0.0.1把API地址从localhost改成10.0.2.2真机用局域网IP最后一行“模拟器访问宿主机”是很多人卡住的地方。Android Studio的模拟器有独立网络栈访问电脑本机服务必须用特殊地址这个知识点在报告的网络调试章节里写清楚会显得你确实做过联调。5. 验证与交付把代码变成一份讲得出来的报告5.1 用单元测试离线验证JSON解析不用每次跑模拟器课程设计提交之前先写一个JUnit测试把一键JSON字符串直接喂给Gson断言关键字段。这样不用启动模拟器也能保证解析层是稳定的。测试代码放在app/src/test目录下运行无需真机。class WeatherJsonTest { Test fun parseWeatherJson() { val json {code:200,now:{temp:23,text:多云}} val response Gson().fromJson( json, WeatherResponse::class.java ) assertEquals(23, response.now.temp) assertEquals(多云, response.now.text) } }这个测试的本质作用是把网络请求和解析逻辑解耦。哪怕以后天气API换了字段只要这个测试还在回归时一眼就能看出是哪个字段失效。答辩时如果老师问你“怎么证明解析逻辑是对的”直接演示这个测试比翻半天Logcat更有说服力。5.2 用adb截图和录屏直接作为报告素材课程设计报告需要界面截图。Android Studio自带的截图工具可以点按钮但批量截图效率太低。用adb命令可以一键完成adb exec-out screencap -p weather_main.png adb shell screenrecord /sdcard/demo.mp4第一条命令把当前模拟器屏幕截成图片直接存放在本地目录第二条命令录屏录完按CtrlC停止然后adb pull /sdcard/demo.mp4导出。我一般会截至少三张图加载中状态、正常天气状态、断网错误状态。这三张图覆盖了界面设计的三个分支报告里配合状态说明非常完整。5.3 报告里必讲的3个设计取舍比堆代码更能拿分第一个取舍是协程 vs AsyncTask。不要只说“AsyncTask废弃了”要说明协程如何与lifecycleScope绑定解决Activity销毁时请求泄漏的问题。第二个取舍是缓存过期策略。为什么用20分钟时间戳而不是每次启动都刷新因为天气预报是时敏数据20分钟既能保证数据新鲜度又能减少无效请求。第三个取舍是定位失败兜底。为什么默认城市选北京而不是提示崩溃因为课程设计演示要保证流程完整兜底逻辑能让用户看到后续功能。有一个很实用的验证方式把手机系统时间改到未来一天再点下拉刷新看缓存是否按时间戳失效重新请求新数据。这个操作能同时验证缓存逻辑和时间格式化答辩现场演示时非常加分。本文还有配套的精品资源点击获取