资讯动态

Android天气APP全链路开发:定位+网络+UI实战

发布时间:2026/9/16 19:10:49 来源:尧图企业网站定制
简介这是一份面向计算机相关专业本科生的高分Android毕业设计源码聚焦天气预报APP系统开发适用于课程设计、期末大作业及毕业设计参考。项目已通过严格调试评审得分97分可直接编译运行涵盖UI界面、天气数据获取与解析、定位服务集成、第三方SDK如ViewPagerIndicator、SlidingMenu调用等核心模块助学习者掌握Android应用开发全流程。压缩包共666个文件含108个Java业务逻辑文件、188个XML布局与资源定义、167个Class字节码含编译产物、98个PNG图标资源以及JAR/AAR依赖库、JSON配置、SO本地库等整体24.26MB结构完整、模块清晰。目前已有194人学习下载资源附带完整工程配置Gradle、Keystore、AIDL接口定义等便于快速理解项目架构、复现调试过程并拓展功能。1. 这不是“套模板交差”的天气APP而是能跑通定位网络请求UI刷新全链路的本科级Android工程实践很多同学拿到“天气预报APP”毕设题目第一反应是去GitHub搜个带UI的Demo改改包名、换换图标就交——结果在答辩现场被问“为什么用HttpURLConnection不用OkHttp”“定位权限没动态申请怎么通过Android 12测试”“JSON解析异常时UI卡死怎么处理”当场失语。这个标题里的“高分项目”四个字本质指向一条完整闭环从设备获取经纬度 → 调用公开天气API如和风天气、心知天气→ 解析JSON响应 → 将温度、湿度、风速等字段映射到Activity控件 → 支持下拉刷新与后台定时更新。它不追求炫酷动效或离线地图但必须覆盖Android四大组件中Activity与Service的协作、网络权限声明与运行时申请、JSON解析容错、RecyclerView列表复用、以及Gradle构建配置等本科教学大纲核心能力点。适合正在写开题报告、已配好Android Studio环境、但对“如何把课本上的Fragment生命周期和实际网络请求串起来”仍模糊的同学——本文所有代码均可直接粘贴进app/src/main/java/目录编译通过且适配Android 8.0至14.0系统。2. 用Android Studio创建最小可运行工程从空Activity到显示“加载中”文本2.1 新建项目时的关键配置选择打开Android Studio建议使用2023.2.1或更高版本选择Empty Activity模板Package name设为com.example.weatherappMinimum SDK选API 21 (Android 5.0)——这是当前国内主流机型兼容性与功能支持的平衡点。注意勾选Use AndroidX artifacts避免Support Library兼容问题。创建后Gradle会自动下载依赖等待Build: finished提示。提示不要选“Basic Activity”或“Bottom Navigation Activity”它们自带大量无关Fragment和Navigation组件会干扰你理解“单Activity网络请求”的主线逻辑。2.2 在AndroidManifest.xml中声明必要权限与组件打开app/src/main/AndroidManifest.xml在application标签外侧添加以下权限声明位置必须在application之前uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /接着在application内部添加一个activity声明确保MainActivity被正确注册activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity注意android:exportedtrue是Android 12强制要求漏写会导致应用无法启动。ACCESS_FINE_LOCATION用于高精度定位ACCESS_COARSE_LOCATION作为降级方案两者需同时声明。2.3 修改activity_main.xml实现基础UI布局替换app/src/main/res/layout/activity_main.xml全部内容为以下代码仅保留一个TextView用于显示状态?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:gravitycenter android:padding16dp TextView android:idid/tv_weather_info android:layout_widthwrap_content android:layout_heightwrap_content android:text正在加载天气信息... android:textSize16sp android:textColor#333 / /LinearLayout此布局刻意精简目的是让你后续能清晰看到“数据从网络来、到UI去”的路径而非被CardView、ConstraintLayout等复杂结构分散注意力。2.4 在MainActivity.java中编写首次加载逻辑打开app/src/main/java/com/example/weatherapp/MainActivity.java替换为以下代码关键注释已嵌入package com.example.weatherapp; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.widget.TextView; import android.widget.Toast; import java.util.Locale; public class MainActivity extends AppCompatActivity { private static final int LOCATION_PERMISSION_REQUEST_CODE 1001; private TextView tvWeatherInfo; private LocationManager locationManager; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvWeatherInfo findViewById(R.id.tv_weather_info); locationManager (LocationManager) getSystemService(LOCATION_SERVICE); // 检查是否已有定位权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { // 请求权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST_CODE); } else { // 权限已授予直接获取位置 getCurrentLocation(); } } Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode LOCATION_PERMISSION_REQUEST_CODE) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { getCurrentLocation(); // 权限通过后获取位置 } else { Toast.makeText(this, 定位权限被拒绝无法获取天气, Toast.LENGTH_SHORT).show(); tvWeatherInfo.setText(权限不足请手动开启定位); } } } private void getCurrentLocation() { try { // 使用GPS或网络定位提供者 Location lastKnownLocation locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (lastKnownLocation null) { lastKnownLocation locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); } if (lastKnownLocation ! null) { double latitude lastKnownLocation.getLatitude(); double longitude lastKnownLocation.getLongitude(); tvWeatherInfo.setText(String.format(Locale.getDefault(), 定位成功纬度%.4f经度%.4f, latitude, longitude)); // 此处将调用网络请求方法下一章实现 fetchWeatherData(latitude, longitude); } else { tvWeatherInfo.setText(未获取到有效位置请检查GPS开关); } } catch (SecurityException e) { tvWeatherInfo.setText(安全异常 e.getMessage()); } } private void fetchWeatherData(double lat, double lon) { // 占位方法将在第3章实现具体网络请求 tvWeatherInfo.setText(正在请求天气数据...); } }这段代码完成了三个硬性要求权限动态申请覆盖Android 6.0运行时权限模型定位双源 fallback先尝试GPS失败则用网络定位NETWORK_PROVIDER避免纯GPS在室内失效空指针防护对lastKnownLocation做非空判断防止NullPointerException崩溃。此时点击运行APP会弹出权限对话框允许后显示经纬度坐标——这证明你的工程骨架已打通“设备硬件→系统服务→Java层调用”链路是后续接入天气API的前提。3. 接入和风天气API用OkHttp发起HTTPS请求并解析JSON响应3.1 添加网络与JSON依赖到build.gradle打开app/build.gradle注意是module级别的非project级在dependencies闭包内添加以下三行implementation com.squareup.okhttp3:okhttp:4.12.0 implementation com.google.code.gson:gson:2.10.1 implementation androidx.lifecycle:lifecycle-viewmodel:2.6.2其中okhttp用于发送HTTP请求gson用于JSON解析lifecycle-viewmodel为后续UI与数据分离打基础。添加后点击右上角Sync Now等待Gradle同步完成。注意不要使用过时的org.json原生解析器——它需要手写大量getJSONObject()嵌套易出错且无法自动生成Java Bean也不要选Retrofit学习成本高OkHttpGson组合最贴近本科教学场景的“可控性”。3.2 创建WeatherData实体类映射JSON结构在java/com/example/weatherapp/下新建包model再新建Java类WeatherResponse.javapackage com.example.weatherapp.model; import java.util.List; public class WeatherResponse { public String status; public String desc; public Data data; public static class Data { public Realtime realtime; public ListFuture forecast; } public static class Realtime { public String temperature; // 当前温度 public String humidity; // 湿度 public String info; // 天气描述晴/多云 public String wid; // 天气图标ID public String power; // 风力等级 public String direct; // 风向 } public static class Future { public String date; public String temperature; public String weather; public String wid; } }该结构严格对应和风天气免费APIhttps://free-api.heweather.net/s6/weather/now?locationlat,lonkeyYOUR_KEY的JSON返回格式。字段名小写驼峰命名与JSON key完全一致Gson才能自动映射。3.3 编写OkHttpClient单例与异步请求方法在java/com/example/weatherapp/下新建包network新建ApiService.javapackage com.example.weatherapp.network; import android.os.Handler; import android.os.Looper; import android.widget.TextView; import androidx.annotation.NonNull; import com.example.weatherapp.MainActivity; import com.example.weatherapp.R; import com.example.weatherapp.model.WeatherResponse; import com.google.gson.Gson; import java.io.IOException; import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class ApiService { private static final String API_KEY your_he_weather_key_here; // 替换为你的和风天气Key private static final String BASE_URL https://free-api.heweather.net/s6/weather/; private static OkHttpClient client; static { client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); } public static void fetchNowWeather(double lat, double lon, TextView textView) { String url String.format(%snow?location%.4f,%.4fkey%s, BASE_URL, lat, lon, API_KEY); Request request new Request.Builder() .url(url) .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(NonNull Call call, NonNull IOException e) { new Handler(Looper.getMainLooper()).post(() - { textView.setText(网络请求失败 e.getMessage()); }); } Override public void onResponse(NonNull Call call, NonNull Response response) throws IOException { if (response.isSuccessful()) { String json response.body().string(); Gson gson new Gson(); WeatherResponse result gson.fromJson(json, WeatherResponse.class); // 主线程更新UI new Handler(Looper.getMainLooper()).post(() - { if (ok.equals(result.status)) { WeatherResponse.Realtime rt result.data.realtime; String info String.format(当前%s%s℃湿度%s%%%s%s级, rt.info, rt.temperature, rt.humidity, rt.direct, rt.power); textView.setText(info); } else { textView.setText(API返回错误 result.desc); } }); } else { new Handler(Looper.getMainLooper()).post(() - textView.setText(HTTP错误 response.code())); } } }); } }关键参数说明connectTimeout与readTimeout设为10秒避免用户长时间等待new Handler(Looper.getMainLooper()).post()确保UI更新在主线程执行否则抛CalledFromWrongThreadExceptiongson.fromJson(json, WeatherResponse.class)一行完成整个JSON树到Java对象的转换无需手动遍历JsonObject。3.4 在MainActivity中调用API请求回到MainActivity.java找到fetchWeatherData()方法将其替换为private void fetchWeatherData(double lat, double lon) { ApiService.fetchNowWeather(lat, lon, tvWeatherInfo); }同时在文件顶部添加导入import com.example.weatherapp.network.ApiService;此时重新运行APP授权定位后TextView将显示类似“当前晴26℃湿度45%东南风2级”的真实天气信息。你已打通“设备定位→网络请求→JSON解析→UI渲染”全链路这是本科毕设答辩中最能体现工程能力的核心模块。4. 实现下拉刷新与后台定时更新用SwipeRefreshLayout与WorkManager构建健壮数据管道4.1 为Activity添加下拉刷新容器修改activity_main.xml将TextView包裹进SwipeRefreshLayout?xml version1.0 encodingutf-8? androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:idid/swipe_refresh android:layout_widthmatch_parent android:layout_heightmatch_parent LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:gravitycenter android:padding16dp TextView android:idid/tv_weather_info android:layout_widthwrap_content android:layout_heightwrap_content android:text正在加载天气信息... android:textSize16sp android:textColor#333 / /LinearLayout /androidx.swiperefreshlayout.widget.SwipeRefreshLayout4.2 在MainActivity中初始化并监听刷新事件在MainActivity.java的onCreate()方法末尾setContentView之后添加SwipeRefreshLayout swipeRefresh findViewById(R.id.swipe_refresh); swipeRefresh.setOnRefreshListener(() - { // 刷新时重新获取位置并请求天气 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { getCurrentLocation(); } else { Toast.makeText(this, 请先授权定位权限, Toast.LENGTH_SHORT).show(); swipeRefresh.setRefreshing(false); // 停止刷新动画 } });并在fetchWeatherData()方法末尾添加停止刷新动画的逻辑private void fetchWeatherData(double lat, double lon) { ApiService.fetchNowWeather(lat, lon, tvWeatherInfo); // 确保刷新动画在数据加载完成后停止 SwipeRefreshLayout swipeRefresh findViewById(R.id.swipe_refresh); if (swipeRefresh.isRefreshing()) { swipeRefresh.setRefreshing(false); } }注意setRefreshing(false)必须在数据加载完成回调中调用否则用户下拉后界面会一直显示旋转图标。4.3 使用WorkManager实现后台定时更新Android 8.0兼容方案Android 8.0后AlarmManager受限WorkManager成为官方推荐的后台任务调度方案。在app/build.gradle中添加依赖implementation androidx.work:work-runtime-ktx:2.8.1新建worker/WeatherUpdateWorker.javapackage com.example.weatherapp.worker; import android.content.Context; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; import com.example.weatherapp.network.ApiService; public class WeatherUpdateWorker extends Worker { public WeatherUpdateWorker(NonNull Context context, NonNull WorkerParameters params) { super(context, params); } NonNull Override public Result doWork() { // 此处不直接更新UIWorker运行在后台线程仅触发网络请求 // 实际项目中可保存到Room数据库再由Activity观察变化 return Result.success(); } }提示本科毕设中Worker只需证明“能定时触发”不必强求实时UI更新。更务实的做法是——在doWork()中调用ApiService.fetchNowWeather()并将结果存入SharedPreferencesActivity通过registerReceiver()监听广播更新UI。但为控制篇幅此处聚焦核心流程。4.4 配置WorkManager周期性任务在MainActivity.java的onCreate()中添加以下代码权限检查之后// 每2小时执行一次天气更新仅作演示实际可设为4小时 PeriodicWorkRequest weatherUpdateRequest new PeriodicWorkRequest.Builder(WeatherUpdateWorker.class, 2, TimeUnit.HOURS) .setConstraints(new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); WorkManager.getInstance(this).enqueueUniquePeriodicWork( weather_update, ExistingPeriodicWorkPolicy.KEEP, weatherUpdateRequest );需在文件顶部添加导入import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.Constraints; import androidx.work.NetworkType;此配置确保APP在后台时系统仍能每2小时唤醒一次获取最新天气符合“高分项目”对后台服务能力的要求。WorkManager会自动处理Android 12的后台执行限制无需手动适配。5. 优化用户体验与答辩加分技巧字体适配、深色模式兼容与APK体积压缩5.1 用sp单位与TextAppearance统一文字样式在res/values/styles.xml中定义天气信息文本样式style nameWeatherTextAppearance parentTextAppearance.AppCompat.Medium item nameandroid:textSize18sp/item item nameandroid:textColor?android:attr/textColorPrimary/item item nameandroid:lineSpacingMultiplier1.3/item /style然后在activity_main.xml的TextView中引用android:textAppearancestyle/WeatherTextAppearancesp单位随系统字体大小缩放?android:attr/textColorPrimary自动适配深色/浅色模式避免硬编码#000000导致深色模式下文字不可见。5.2 为不同屏幕密度提供适配资源在res/目录下新建values-sw600dp/平板、values-sw720dp/大屏文件夹各自放入dimens.xml!-- res/values-sw600dp/dimens.xml -- dimen nameactivity_horizontal_margin48dp/dimen dimen nameactivity_vertical_margin24dp/dimen这样在平板上android:padding16dp会自动升级为48dp提升大屏可读性。本科毕设答辩时展示多设备预览能直观体现“响应式设计”能力。5.3 使用R8压缩APK并移除无用资源在app/build.gradle的android闭包内添加buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } }并在proguard-rules.pro中保留Gson必需的反射类防止JSON解析失败-keep class com.example.weatherapp.model.** { *; } -keep class com.google.gson.** { *; } -keep class okhttp3.** { *; } -keep class retrofit2.** { *; }启用R8后Release版APK体积可减少30%以上且移除日志打印、调试代码符合生产环境规范。答辩时可演示Build Generate Signed Bundle/APK流程并对比Debug与Release包大小。5.4 三个让答辩老师眼前一亮的细节技巧技巧实现方式为什么加分网络状态实时反馈在ApiService的onFailure()中检测e.getCause() instanceof UnknownHostException提示“请检查网络连接”而非泛泛的“请求失败”体现异常分类处理能力非简单try-catch温度数字用等宽字体给TextView添加android:fontFamilymonospace使“26℃”数字对齐更专业UI细节意识远超同龄人粗糙排版权限拒绝后引导设置页在onRequestPermissionsResult()中当grantResults[0] PackageManager.PERMISSION_DENIED时跳转系统设置页Intent intent new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);brUri uri Uri.fromParts(package, getPackageName(), null);brintent.setData(uri); startActivity(intent);展示真实用户场景应对能力非纸上谈兵这些技巧无需额外框架几行代码即可落地却能在答辩时精准戳中评委对“工程素养”的期待点——毕竟高分毕设的本质从来不是堆砌技术名词而是让每一行代码都服务于可感知的用户体验。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价