尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android天气小程序开发:从Retrofit网络请求到数据展示全流程
简介一款使用安卓官方集成开发环境Android Studio编写的天气预报小程序完整源码面向初学安卓开发的读者以及需要快速搭建天气类功能演示的开发者能够帮助理解从创建项目、配置网络依赖、发送接口请求到解析天气数据、更新界面控件的全过程。包内共包含1260个文件压缩后约为9.76兆字节以Java程序文件、XML界面布局和Gradle构建配置为主要内容同时附带了编译过程中生成的dex类文件、class字节码、flat资源文件以及可直接安装的debug版APK方便对照源码查看构建结果。目前已有3876人浏览学习。通过阅读这份源码可以掌握第三方天气接口的调用方式、异步回调处理、界面数据绑定、进度提示与下拉刷新等常见实现技巧也能借助完整的目录结构和构建产物快速定位问题适合作为课程设计或入职练手项目。1. 认清这个标题AndroidStudio 天气小程序难的不是写界面是把数据跑通很多人搜「AndroidStudio实现天气预报小程序源码」以为难点在布局或者动画实际动手后会发现真正卡人的是三条线天气数据从哪来、JSON 怎么解析、列表为什么刷不出来。这篇文章把这三条线拆开从选型到封装再到避坑按我平时做小项目的顺序一步步给你保证你照着敲能跑起来。无论你是课程设计、毕设预演还是单纯想练手 Android 网络请求这套方案都够用。读完后你不仅能复现还能知道每个参数为什么这么设、翻车时先查哪个文件。2. 数据源先行天气 API 的选型与请求封装跑通第一条 JSON 数据天气小程序的核心是数据界面只是数据的搬运工。先想明白一件事你需要的不是「最全」的数据源而是「最快能调通、字段稳定、免费额度够用」的数据源。我见过不少人一上来就接高精度逐小时预报结果解析代码写了一周需求其实只需要今天和未来三天的天气白白浪费精力。2.1 为什么优先选 HTTP 接口而不是官方 SDK市面上多数天气数据服务商会同时提供 SDK 和 HTTP 接口。新手容易本能地选 SDK觉得封装好了省事。但我的建议恰恰相反在「学习型项目」里优先选 HTTP 接口原因有三个。第一SDK 是个黑匣子初始化、权限、混淆规则哪一环出错都很难排查出了问题你连请求长什么样都看不到。HTTP 接口用 Retrofit 发请求你能在日志里完整看到 URL、参数和返回体出错时一眼定位。第二SDK 的接入方式依赖服务商的 Android 版本适配换数据源就要重写接入层HTTP 接口只要换 baseUrl 和字段映射改动量小得多。第三也是最重要的笔试面试时面试官问的往往就是「你怎么封装网络层、怎么处理嵌套 JSON」用 HTTP 接口做出来的项目技术点是能讲清楚的。注册完数据服务商后你会拿到一个 API Key 和一个接口文档。我的建议是先在浏览器里把请求 URL 拼出来直接访问看到返回 JSON 后再写代码。这样你能先确认 Key 有没有生效、返回的字段长什么样避免代码写完了才发现是数据源权限的问题。2.2 用 Retrofit 定义天气请求接口配上日志拦截器项目里网络层依赖我习惯用 Retrofit OkHttp Gson 的组合这也是目前最常见的方案。先看依赖配置dependencies { 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 }逻辑说明retrofit 负责把接口定义转换成 HTTP 请求converter-gson 负责把 JSON 自动映射成 Java 对象logging-interceptor 负责打印请求和响应日志。三个缺一不可少了 converter拿到的是 ResponseBody 原始字符串解析得自己写少了日志拦截器请求失败时你什么都看不到。Gradle 同步完成后定义一个接口文件这是整个项目的核心抽象public interface WeatherApi { GET(v3/weather/daily) CallWeatherResponse getDailyWeather( Query(key) String apiKey, Query(location) String location, Query(unit) String unit ); }参数说明GET 里的路径是相对路径最终请求地址是 baseUrl 拼接上这个路径。location 是城市 ID 或经纬度我这里用的是经纬度字符串「经度,纬度」。unit 表示单位制m 代表公制、摄氏温度如果是英制单位会返回华氏度容易引起误解。接口写好后再创建一个单例类来统一管理 Retrofit 实例public class ApiClient { private static final String BASE_URL https://api.xxxweather.com/; private static WeatherApi weatherApi; public static WeatherApi getClient() { if (weatherApi null) { HttpLoggingInterceptor logging new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client new OkHttpClient.Builder() .addInterceptor(logging) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); weatherApi retrofit.create(WeatherApi.class); } return weatherApi; } }逻辑说明用单例是为了避免每次请求都重新创建 OkHttpClient连接池和线程池可以复用频繁创建会有资源开销。BASE_URL 必须以 / 结尾否则 Retrofit 会在运行时直接抛异常。connectTimeout 是建立连接的超时readTimeout 是等待响应数据的超时建议都设在 10 秒左右太短在弱网下容易误判失败太长会让用户一直卡在加载中。提示初学阶段日志级别设成 Level.BODY能看到完整请求参数和响应 JSON上线前记得降级为 Level.BASIC 或直接删掉避免敏感信息和流量消耗。2.3 解析嵌套 JSON响应体数据类的设计天气接口的返回通常是一个三层嵌套结构外层是状态码中间是数据主体最里层才是每日天气数组。用 Gson 解析时数据类字段名必须和 JSON 字段完全一致或者用 SerializedName 做映射。先看一段简化后的返回结构{ code: 200, daily: [ { fxDate: 2025-06-10, tempMax: 28, tempMin: 22, textDay: 多云, iconDay: 101 } ] }对应的数据类这样写public class WeatherResponse { SerializedName(code) public String code; SerializedName(daily) public ListDailyForecast daily; public static class DailyForecast { SerializedName(fxDate) public String date; SerializedName(tempMax) public String tempMax; SerializedName(tempMin) public String tempMin; SerializedName(textDay) public String textDay; SerializedName(iconDay) public String iconDay; } }逻辑说明这里用 public 字段是为了让代码简短直观正规项目建议改成 private getter。SerializedName 的作用是把 JSON 里的驼峰或缩写字段名和 Java 字段名解耦比如 JSON 里的 tempMax 想映射成 Java 的 maxTemp全靠这个注解解决不用改 JSON 字段。这一步容易被忽略的是字段类型要按响应实际值来定。温度、湿度这类数值用 String 比用 int 更稳因为有些数据源晴天返回 28阴天可能返回空字符串用 int 直接解析就会崩。这就是为什么我示例里所有数值字段都用 String。写好数据类和接口后先写一小段测试代码验证请求能通WeatherApi api ApiClient.getClient(); api.getDailyWeather(你的Key, 116.41,39.92, m) .enqueue(new CallbackWeatherResponse() { Override public void onResponse(CallWeatherResponse call, ResponseWeatherResponse response) { if (response.isSuccessful() response.body() ! null) { WeatherResponse.DailyForecast today response.body().daily.get(0); Log.d(WEATHER_TEST, today.textDay today.tempMin ~ today.tempMax); } } Override public void onFailure(CallWeatherResponse call, Throwable t) { Log.e(WEATHER_TEST, 请求失败, t); } });这段代码跑通了你就算跨过了第一个大坑数据链路是通的。后面所有界面工作都建立在这一步之上。3. 界面搭建XML 布局 RecyclerView 渲染天气列表先写死数据再对接网络层通了以后很多人会急着把数据往界面上怼结果常常是布局乱了、数据对不上位置。我习惯反过来先把界面搭好用写死的数据跑通 UI再替换成真实数据。这样能把「界面问题」和「数据问题」分开排查省掉大量两头找原因的翻车时间。3.1 布局拆分顶部放当前天气中间放未来列表天气类 App 的界面结构很固定顶部一块显示城市、温度、天气现象下面用列表展示未来多天的预报。这种结构用 LinearLayout 套 RecyclerView 就能实现不需要复杂的 ConstraintLayout 嵌套。!-- activity_main.xml -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:background#E8F4FD TextView android:idid/tvCity android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitycenter android:textSize20sp android:padding12dp android:text北京 / TextView android:idid/tvTodayTemp android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitycenter android:textSize48sp android:text22° ~ 28° / TextView android:idid/tvTodayText android:layout_widthmatch_parent android:layout_heightwrap_content android:gravitycenter android:textSize16sp android:text多云 android:paddingBottom16dp / androidx.recyclerview.widget.RecyclerView android:idid/rvForecast android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayout逻辑说明顶部三个 TextView 分别显示城市、温度区间、天气现象RecyclerView 占据剩余高度显示未来列表。背景色用浅蓝是为了贴合天气主题这是纯视觉因素后期想换成跟随天气变化的动态背景改这一处就行。每个列表项单独建一个 item_forecast.xml结构要保持简单!-- item_forecast.xml -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding16dp android:background#FFFFFF android:layout_marginTop4dp TextView android:idid/tvDate android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:textSize16sp / TextView android:idid/tvTempRange android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:gravitycenter android:textSize16sp / TextView android:idid/tvWeatherText android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:gravityend android:textSize16sp / /LinearLayout参数说明layout_weight1 让三个文本均分宽度避免日期长了把温度挤到看不见。这里没有给根布局设置圆角是因为圆角要在 drawable 里写 shape 文件再引用属于细节优化我放在后面的进阶章节讲。3.2 写 Adapter 并绑定列表项数据布局就绪后写 RecyclerView 的 Adapter。这个 Adapter 在项目里相对固定核心是把 ListWeatherResponse.DailyForecast 绑定到 item 的三个 TextView 上public class ForecastAdapter extends RecyclerView.AdapterForecastAdapter.ViewHolder { private ListWeatherResponse.DailyForecast dataList new ArrayList(); public void setData(ListWeatherResponse.DailyForecast data) { this.dataList data; 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) { WeatherResponse.DailyForecast item dataList.get(position); holder.tvDate.setText(item.date.substring(5)); holder.tvTempRange.setText(item.tempMin ° ~ item.tempMax °); holder.tvWeatherText.setText(item.textDay); } Override public int getItemCount() { return dataList.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView tvDate; TextView tvTempRange; TextView tvWeatherText; public ViewHolder(NonNull View itemView) { super(itemView); tvDate itemView.findViewById(R.id.tvDate); tvTempRange itemView.findViewById(R.id.tvTempRange); tvWeatherText itemView.findViewById(R.id.tvWeatherText); } } }逻辑说明setData 方法里调用了 notifyDataSetChanged让 RecyclerView 知道数据集发生了变化需要重绘。date.substring(5) 是把「2025-06-10」截成「06-10」显示这样更符合手机上日期展示习惯也省掉 SimpleDateFormat 转换的额外开销。onCreateViewHolder 是工厂方法只负责创建 ViewHolderonBindViewHolder 负责把数据填进 ViewHolder 里的控件。这两者分工要清楚创建次数远远少于绑定次数把耗时操作放在绑定阶段就是在拖累滑动流畅度。3.3 先写死数据验证界面再无缝接入网络请求在 MainActivity 里先用临时数据把列表刷出来这是排错时非常重要的一步。写死了数据还对不齐说明是布局或 Adapter 的锅数据源替换后才出错那锅就是解析或字段映射的。public class MainActivity extends AppCompatActivity { private ForecastAdapter adapter; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView rvForecast findViewById(R.id.rvForecast); rvForecast.setLayoutManager(new LinearLayoutManager(this)); adapter new ForecastAdapter(); rvForecast.setAdapter(adapter); // 临时写死数据验证列表显示 ListWeatherResponse.DailyForecast fakeList new ArrayList(); for (int i 0; i 3; i) { WeatherResponse.DailyForecast item new WeatherResponse.DailyForecast(); item.date 2025-06-1 i; item.tempMin 2 i; item.tempMax 3 i; item.textDay 多云; fakeList.add(item); } adapter.setData(fakeList); } }逻辑说明先 new 出一个 List手动填三条数据setData 后 RecyclerView 就应该显示三行。注意这里的字段名我写的是 date、tempMin 而不是 JSON 的 fxDate、tempMax说明数据类在你的项目里用了 SerializedName 做了映射。如果你直接复制我第 2 章的数据类这里的字段名应该保持 date 和 tempMin别弄混了。界面验证通过后把写死数据这段删掉换成网络请求这是最常见的流程。请求回数据后调用 adapter.setData(response.body().daily)同时把顶部三个 TextView 也一块更新点到为止。4. 串起全流程定位、缓存与下拉刷新让天气数据真正「活」起来写死数据能显示网络请求能返回这是两个孤立的节点。真正让小程序「能用」的是把定位拿到城市、请求天气、展示结果、缓存数据、支持下拉刷新这条链路完整串起来。这一步的体验差距比界面样式大多了。4.1 从定位拿到经纬度权限申请的最小实现天气接口很多支持传城市名但用户体验更好的方式是定位后自动显示所在城市的天气。用系统自带的 LocationManager 就能拿到经纬度不需要额外引入高德或百度定位 SDK给学习项目减负。先在 AndroidManifest.xml 里声明权限uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /逻辑说明INTERNET 权限不加的话网络请求直接失败这个很多人会漏。ACCESS_COARSE_LOCATION 是粗略定位普通天气场景完全够用ACCESS_FINE_LOCATION 是为了兼容部分机型拿不到粗略定位的情况。两个都声明运行时只需要请求其中一个的授权。Android 6.0 以上需要在代码里动态请求权限这是最容易踩坑的环节。最小实现如下private static final int REQUEST_LOCATION_CODE 100; private void requestLocationPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_LOCATION_CODE); } else { startLocation(); } } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQUEST_LOCATION_CODE) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { startLocation(); } else { // 用户拒绝时用默认城市北京继续请求别让 App 白屏 fetchWeatherByCity(101010100); } } }逻辑说明checkSelfPermission 判断是否已经授权未授权则弹系统弹窗。用户点击允许后会回调 onRequestPermissionsResult此时才能安全地调用定位逻辑。拒绝授权的情况必须兜底用默认城市继续走天气流程否则用户拒绝后就只能看到一个空界面体验直接崩掉。拿到定位结果的回调里从 Location 对象取经纬度拼成天气接口需要的 location 参数这一步相当于把定位和天气请求两道工序衔接起来private void startLocation() { LocationManager lm (LocationManager) getSystemService(LOCATION_SERVICE); if (lm null) { fetchWeatherByCity(101010100); return; } Location location lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); if (location ! null) { String loc location.getLongitude() , location.getLatitude(); fetchWeatherByLocation(loc); } else { fetchWeatherByCity(101010100); } }参数说明getLastKnownLocation 拿到的是最近一次缓存的位置不发起新的定位请求速度快但可能不精确天气场景足够。如果返回 null说明系统还没有过定位记录此时同样回退到默认城市保证链路不中断。这里没有用 requestSingleUpdate 这类接口是因为学习项目优先保证流程简单多一层回调就多一个出错面。4.2 用 SharedPreferences 做基础缓存写法和失效策略天气数据的特点是「时效性不强」用户能接受十分钟前的温度但不能接受每次都转圈三秒。加一层缓存一方面让二次打开秒出内容另一方面在断网时也能展示上次的天气数据。SharedPreferences 足够承担这个任务。public class WeatherCache { private static final String PREF_NAME weather_cache; private static final String KEY_DATA weather_json; private static final String KEY_TIME cache_time; private SharedPreferences sp; public WeatherCache(Context context) { sp context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); } public void save(String json) { sp.edit() .putString(KEY_DATA, json) .putLong(KEY_TIME, System.currentTimeMillis()) .apply(); } public String getValidCache(long maxAgeMillis) { long saveTime sp.getLong(KEY_TIME, 0); if (saveTime 0) return null; long age System.currentTimeMillis() - saveTime; if (age maxAgeMillis) return null; return sp.getString(KEY_DATA, null); } }逻辑说明缓存内容存原始 JSON 字符串而不是存解析后的对象列表。原因是 Gson 解析对象再序列化会产生不必要的格式转换存 JSON 后下次读取直接用 Gson 解析一次即可。过期策略用时间戳判断保存时记录写入时间读取时对比当前时间。maxAgeMillis 这个参数怎么设值得讲究天气数据我一般设 30 分钟也就是 30 * 60 * 1000。超过 30 分钟就丢弃缓存重新请求低于 30 分钟直接用缓存。这个值不是固定不变的如果你的数据源有分钟级更新的预警信息可以缩到 10 分钟如果只是日常温度展示1 小时也无妨。提示apply() 是异步写入不要用 commit()。apply 不会卡主线程且系统会保证写入完成commit 会阻塞线程列表滑动时调用它会有肉眼可见的掉帧。在主要流程里缓存的使用方式是「先取缓存展示再发请求更新」。代码写在请求之前的判断逻辑String cached cache.getValidCache(30 * 60 * 1000); if (cached ! null) { WeatherResponse response new Gson().fromJson(cached, WeatherResponse.class); renderUI(response); } // 不管有没有缓存都发一次请求拉最新数据 requestWeather(location);逻辑说明这种做法叫「stale-while-revalidate」先拿过期一点的数据撑住界面再让新数据到位后覆盖它。用户在弱网环境下打开 App 不会干等这是天气类小程序最值得做的一项优化成本几乎为零。4.3 下拉刷新与加载状态的切换列表刷新的标准实现是 SwipeRefreshLayout它是官方下拉刷新控件不需要额外引用第三方库。把 RecyclerView 包在 SwipeRefreshLayout 里面即可androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:idid/refreshLayout android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/rvForecast android:layout_widthmatch_parent android:layout_heightmatch_parent / /androidx.swiperefreshlayout.widget.SwipeRefreshLayout代码层面对应SwipeRefreshLayout refreshLayout findViewById(R.id.refreshLayout); refreshLayout.setColorSchemeResources(R.color.design_default_color_primary); refreshLayout.setOnRefreshListener(() - { requestWeather(currentLocation); });逻辑说明setOnRefreshListener 是下拉触发后的回调这里重新请求一次天气数据。注意一个常见错误请求成功后必须调用 refreshLayout.setRefreshing(false) 关闭刷新动画否则转圈不会停。请求方法内部要把这三个状态切换想清楚private void requestWeather(String location) { if (location null || location.isEmpty()) { refreshLayout.setRefreshing(false); return; } currentLocation location; api.getDailyWeather(API_KEY, location, m) .enqueue(new CallbackWeatherResponse() { Override public void onResponse(CallWeatherResponse call, ResponseWeatherResponse response) { refreshLayout.setRefreshing(false); if (response.isSuccessful() response.body() ! null) { renderUI(response.body()); cache.save(new Gson().toJson(response.body())); } } Override public void onFailure(CallWeatherResponse call, Throwable t) { refreshLayout.setRefreshing(false); Toast.makeText(MainActivity.this, 网络异常, Toast.LENGTH_SHORT).show(); } }); }逻辑说明onResponse 和 onFailure 都要关掉刷新动画。onResponse 里先判断 isSuccessful()HTTP 200 才处理数据401 表示 Key 有问题500 表示数据源服务异常这些情况 response.body() 可能是 null不判断直接取就是空指针。请求成功后把 JSON 存缓存下一次冷启动就能秒开。整个流程到这里一个能日常使用的天气小程序就闭环了。5. 避坑指南Android 天气小程序最常见的 5 个翻车点我从自己带过的项目里挑五个高频问题每个都按「现象 → 原因 → 解决」来讲。这些问题你几乎一定会碰到。5.1 一运行就崩溃R 类或主题样式找不到现象App 启动瞬间闪退Logcat 提示类似 Failed to find style xxx in current theme 或 R 类无法解析。原因新建项目时选的模板不同主题名称不一样或者 styles.xml 里定义的 parent 主题名写错。用错模板时 setContentView 里引用的布局文件 id 和实际生成的 R 类对不上也会报同样的错。解决先检查 res/values/themes.xml 里的 parent 属性。新版模板常见写法是 Theme.XXX 或 Theme.AppCompatMainActivity 继承的是 AppCompatActivity主题必须是 Theme.AppCompat 派生系。然后在 MainActivity 的 setContentView 之前打印一下 R 类是否导入正确在 Android Studio 里点一下 R 类跳转定位跳不过去说明依赖同步没完成重新 Sync Project 即可。这个问题绝大多数情况下是别的原因连着出现的次生问题比如你复制了我前面的代码却没有检查自己的包名和项目结构。5.2 请求返回 400却看不到任何错误信息现象Retrofit 的 onFailure 里抛 IOException或者 onResponse 里 response.isSuccessful() 为 falseLogcat 只显示请求失败原因不明。原因最常见的两个一是忘记声明 INTERNET 权限二是 Android 9.0 及以上默认禁止明文 HTTP 访问。我前面第 2 章的接口如果用的是 htt pis 开头的真实域名就没事但如果你找的数据源只提供了 http 接口请求会被系统直接拦截表现为「看起来像网络错误」实际是安全策略拦截。解决如果确认数据源必须用 http在 AndroidManifest.xml 的 application 节点里加配置application android:usesCleartextTraffictrue ... /application参数说明usesCleartextTraffic 设为 true 是全局允许明文流量学习项目足够生产环境的合理做法是配置 networkSecurityConfig 只允许特定域名走明文其他域名仍然要求加密。这个开关不会影响 https 请求所以开了也没什么副作用只是不够严谨。5.3 RecyclerView 一直空白log 里显示请求成功有数据现象Logcat 打印出了返回的 JSON字段也没问题但界面上列表区域一片空白连顶部 TextView 都没数据。原因databinding 或 adapter 没设置、adapter 的 setData 没有调用 notifyDataSetChanged、或者布局里控件的 id 对不上。我见过最多的是这个顺序错了先 setAdapter 一次再 setData然后重置 RecyclerView 的 Adapter新 Adapter 的数据集是空的看起来像列表没刷新。解决确认 setAdapter 和 setData 调用的 Adapter 是同一个实例setData 方法里一定要调 notifyDataSetChanged在 onBindViewHolder 里打一条 Log看每次绑定的是什么 item。如果绑定的数据是对的但界面空白检查布局文件里 RecyclerView 高度是否为 0。常见写法是 RecyclerView 放在 LinearLayout 里但高度只写了 wrap_content没内容时不占高度列表自然不可见。5.4 定位权限弹窗出现两次或授权后拿不到定位现象第一次打开弹权限选了允许结果又弹了一次或者允许后 onLocationResult 一直不回调卡在加载中。原因权限弹窗出现两次通常是代码里同时请求了 FINE 和 COARSE 两个权限而弹窗按请求数组逐条弹出。拿不到定位多半是 getLastKnownLocation 返回 null因为系统从未有过定位记录而你没有做兜底。解决只请求 COARSE 一个权限足够天气场景使用。getLastKnownLocation 返回 null 时用 LocationManager 的 requestSingleUpdate 或直接用默认城市。我给项目里加的兜底是定位失败就请求北京的数据并给用户一个 Toast 提示App 至少不会白屏。定位逻辑写到这一步稳定性比绝对精确更重要。5.5 Gson 解析出现异常或字段全是 null现象请求成功也可以拿到 JSON 字符串但 WeatherResponse 里 daily 是空的或者每个字段都是 null界面没有任何数据。原因字段名对不上。数据源返回 fxDate你写 date数据源返回 textDay你写 weather。SerializedName 没有加Gson 就老老实实地按 Java 字段名去找 JSON 字段找不到就填 null 或跳过。还有一种是服务商启用了 gzip 压缩响应头里 Content-Encoding: gzip但 Retrofit 没配解压导致拿到的是乱码解析全扑街。解决把返回的 JSON 字符串复制到本地在 Android Studio 的 GsonFormatPlus 插件里生成数据类确认字段一一对应。gzip 问题一般由 OkHttp 自动处理如果你手动设置了请求头 Accept-Encoding: gzip就必须自己解压这就是画蛇添足的典型。排查思路是打印 response.raw().body().string() 看原始返回是否为可读 JSON这一步能区分是解压问题还是字段问题。6. 进阶技巧把天气图标映射做对再做多城市缓存管理列表和基础流程跑通后有两件事最值得提升一是天气代码到图标的映射二是多城市数据的缓存结构。这两件事直接影响项目的完成度和后续扩展空间。很多数据源返回的不只是 textDay 这样的文字描述还会带一个 iconDay 代码比如 100 代表晴、101 代表多云。直接把这个代码当文件名去加载 drawable 是最直觉的方案但有个边界坑不同数据源的代码体系不同而且代码并不连续100、101、102 中间可能跳过很多号。我习惯的做法是先建一个映射表把代码集中管理public class WeatherIconMapper { private static final MapString, Integer ICON_MAP new HashMap(); static { ICON_MAP.put(100, R.drawable.ic_sunny); ICON_MAP.put(101, R.drawable.ic_cloudy); ICON_MAP.put(104, R.drawable.ic_overcast); ICON_MAP.put(300, R.drawable.ic_shower); ICON_MAP.put(305, R.drawable.ic_rain); ICON_MAP.put(400, R.drawable.ic_snow); } public static int getIconRes(String iconCode) { Integer resId ICON_MAP.get(iconCode); return resId null ? R.drawable.ic_unknown : resId; } }逻辑说明getIconRes 返回一个 int 类型的资源 id能直接用 setImageResource 加载。关键在映射不到时回退到 ic_unknown避免因为服务商新增了一个图标代码导致 App 崩溃或显示空白。这个兜底的习惯在对接任何外部数据源时都适用。多城市管理的缓存结构超过三个城市后 SharedPreferences 的扁平 key 就不够用了。常见做法是用城市 ID 作为 key 前缀把每个城市的缓存分开存这样切换城市时只加载对应的那一份public void saveWeather(String cityId, String json) { sp.edit() .putString(weather_ cityId, json) .putLong(time_ cityId, System.currentTimeMillis()) .apply(); } public String getCachedWeather(String cityId) { long saveTime sp.getLong(time_ cityId, 0); if (System.currentTimeMillis() - saveTime MAX_AGE_MILLIS) { return null; } return sp.getString(weather_ cityId, null); }参数说明cityId 是数据源为每个城市分配的唯一标识比如北京是 101010100用它做 key 前缀天然避免城市名重复或编码不一致带来的覆盖问题。时间戳和 JSON 分开存读取时先判断是否过期逻辑和二线城市版缓存完全一致只是 key 的粒度从全局变成了按城市。做完这些这个从小程序已经具备定位、缓存、刷新、图标映射和多城市切换的能力往课程设计或者简历项目方向补都够实了。说句实在话把天气这种高频、低复杂度、方向明确的 demo 跑透学到的网络层封装、生命周期管理和异步回调意识比生啃一遍框架文档管用得多。我自己早期做原型也是这套路唯一的后悔药就是当初没把日志拦截器早点加上导致排查慢了一整个下午。先加日志再写业务这习惯能让你少走太多弯路。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

智能体记忆系统设计:三层架构、抽取存储与召回策略实战

智能体记忆系统设计:三层架构、抽取存储与召回策略实战

1. 记忆系统到底在解决什么问题智能体这东西,大家最开始接触的时候,多半是从“单轮问答”开始的。你问一句,它答一句,对话结束,一切归零。这种模式在演示场景里看着挺唬人,但一旦放到真实业务里&#xff0c…

📅 2026/10/12 8:02:48
不装Outlook也能解析PST/OST邮件:XstReader实战指南

不装Outlook也能解析PST/OST邮件:XstReader实战指南

简介:XstReader是一款完全用C#编写的开源Outlook .ost/.pst文件查看器,仅需.Net Framework 4,无需任何Microsoft Office组件,面向需要在无Outlook环境下读取邮件数据的开发者和普通用户。它解决了传统Outlook无法直接打开.ost缓存…

📅 2026/10/12 8:02:48
自动化代码审查工具open-code-review:设计与规则引擎实现

自动化代码审查工具open-code-review:设计与规则引擎实现

有段时间我特别不想轮到自己当 review 的人。白天写代码已经耗光了耐心,晚上还要逐行读别人提交的合并请求,最常见的情形是:低级问题一眼就能看到,可一旦改动超过几百行,人眼就开始漏东西。更别提那些藏在几百行之外的…

📅 2026/10/12 8:02:48
MORE NEWS

更多资讯

📰

如何利用 DeepSeek + Continue 插件大幅提升编码效率:TaoToken 统一 Key 接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

ComfyUI+Qwen2.1:动漫转写实稳定工作流,告别提示词抽卡

玩AI绘画的同学应该都有过这种经历:一张特别有感觉的动漫原画,想把它转成写实风格,结果提示词写了一堆,死活转不出来,出来的图要么脸崩,要么气质全变,要么角度跟原图根本不是一回事。抽卡抽了几…

📰

JDK动态代理源码解析:从Proxy.newProxyInstance到$Proxy0,搞懂Spring AOP与MyBatis的底层机制

刚开始接触Spring AOP或者MyBatis源码那会儿,我死活想不通一个问题:明明我调的是Service接口里的方法,为什么日志、事务这些逻辑就那么平白无故地插进来了?代码里也没人new一个中间类啊。后来第一次动手手撕Proxy、InvocationHand…

📰

PPO强化学习实战:从零训练会跳舞的鸭子

1. 从“学会跳舞的鸭子”说起:PPO到底在解决什么问题第一次看到“学会跳舞的鸭子”这个说法,我愣了两秒,然后会心一笑。这个比喻太传神了——想象一下,一只鸭子本来只会摇摇摆摆走路,现在要让它学会转圈、拍翅膀、踩节…

📰

OpenClaw Session 管理完全指南:Context 压缩、重置与持久化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

影刀RPA实战指南:从流程自动化到职业竞争力提升

去年年底,我们部门接了个紧急任务:每天要从三个系统里导出数据、按客户维度填表、再通过邮件回传,加起来四十多个步骤,手动操作一整天才能跑完。我原以为这活儿得干到年终,没想到花了两个周末把整个流程用影刀RPA串了起…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬