1. 这个天气App到底解决了什么问题刚接触Android开发的朋友十有八九都是从“做一个天气App”开始的。原因很简单它足够小小到一个周末就能跑通又足够全全到能把网络请求、数据解析、UI渲染、权限管理这些核心环节全部串一遍。但真正动手之后你会发现坑远比想象中多——接口怎么选、数据怎么解析、城市怎么查、折线图怎么画每一步都能卡住半天。这个项目就是冲着这些坑去的。它把百度地图API、和风天气API、城市查询和折线展示这四个模块拼在一起形成一个完整的天气查询工具。用户打开App可以通过地图定位当前城市也可以手动搜索城市名然后拉取实时天气和未来几天的预报数据最后用折线图把温度变化直观地画出来。适合谁来参考如果你已经装好了Android Studio能跑通一个Hello World但对网络请求、JSON解析、第三方SDK集成还比较模糊那这个项目的结构就是为你准备的。它不追求UI多华丽重点在于把“数据从哪来、怎么拿、怎么显示”这条链路讲清楚。下面我会按照实际开发的顺序把每个环节的选型理由、关键代码和踩过的坑逐一拆开说。2. 整体架构与技术选型思路2.1 为什么是百度地图加和风天气这个组合先说说为什么选这两个API。百度地图API在国内的覆盖率和定位精度都比较稳尤其是它的城市反地理编码功能能把经纬度直接转成城市名省去了自己维护城市坐标表的麻烦。和风天气API的优势在于免费额度对个人开发者足够友好而且返回的JSON结构清晰字段命名规范解析起来不费劲。这两个服务配合的逻辑是这样的百度地图负责“你在哪”和风天气负责“那里天气怎么样”。中间用城市名或城市ID作为桥梁。百度地图返回的城市名可以直接传给和风天气的城市查询接口拿到对应的Location ID再用这个ID去请求天气数据。整个链路是通的不需要额外的中间层。注意百度地图的定位SDK需要申请AK和风天气需要申请Key这两个凭证都要在对应的开发者后台配置包名和签名SHA1否则请求会直接返回错误码。2.2 项目模块划分与数据流向整个App我把它拆成四个模块定位模块、城市搜索模块、天气数据模块和图表渲染模块。定位模块负责拿到当前经纬度并反编码成城市名城市搜索模块提供一个输入框用户手动输入城市名后调用和风天气的城市查询接口天气数据模块根据城市ID拉取实时天气和逐日预报图表模块把逐日预报里的最高温和最低温抽出来用折线图展示。数据流向是单向的定位或搜索产生城市信息城市信息触发天气请求天气请求返回的数据驱动图表更新。这种单向流动的好处是调试方便哪个环节出问题看日志就能定位到具体模块。我试过把定位和天气请求耦合在一起结果定位失败的时候天气也拉不到排查起来很头疼后来拆开之后就清爽多了。2.3 网络框架和图表库的选择理由网络请求这块我选的是OkHttp Gson这个组合。OkHttp负责发请求和处理回调Gson负责把JSON字符串转成Java对象。没有用Retrofit是因为这个项目的接口数量不多手写请求反而更直观新手也更容易看懂每一步在做什么。Gson的字段映射用SerializedName注解就能搞定和风天气返回的字段名和Java命名规范不一致的地方加个注解就解决了。折线图用的是MPAndroidChart。这个库在Android圈子里用得比较广文档和示例都多遇到问题搜一下基本能找到答案。它的LineChart控件支持多条折线、自定义轴标签、动画效果对于温度展示这个场景来说功能绰绰有余。唯一需要注意的是它的坐标轴配置项比较多第一次用容易配乱后面我会把关键配置单独拎出来讲。3. 开发环境搭建与依赖配置3.1 Android Studio版本与Gradle配置要点我用的Android Studio版本是较新的稳定版Gradle插件版本和Gradle版本要匹配不然同步的时候会报一堆莫名其妙的错。具体来说build.gradle项目级里的com.android.tools.build:gradle版本要和gradle-wrapper.properties里的distributionUrl版本对应上。我一般直接看Android Studio新建项目时默认给的组合那个组合是经过验证的不会出问题。minSdkVersion我设的是21也就是Android 5.0。这个版本覆盖了绝大多数在用设备同时也能用上一些比较新的API。targetSdkVersion设成最新的稳定版就行但要注意Android 6.0以上定位权限需要动态申请不能只在Manifest里声明。3.2 百度地图SDK集成步骤与AK申请百度地图的集成分三步。第一步是在开发者后台创建应用填写包名和SHA1。SHA1的获取方式是在Android Studio的Terminal里执行keytool命令或者直接在Gradle面板里找signingReport任务运行一下输出里就有。第二步是下载SDK把jar包和so库放到libs目录和jniLibs目录下。第三步是在Manifest里配置AK和定位服务。meta-data android:namecom.baidu.lbsapi.API_KEY android:value你的AK / service android:namecom.baidu.location.f android:enabledtrue android:process:remote /提示so库要按CPU架构分目录放如果只放armeabi-v7a在64位设备上可能会找不到库。保险起见把arm64-v8a也加上。3.3 和风天气Key配置与网络权限声明和风天气这边相对简单在后台拿到Key之后直接在请求URL里拼上就行。Manifest里需要声明INTERNET权限和ACCESS_NETWORK_STATE权限。如果要做定位还要加ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION。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 /Gradle依赖这块把OkHttp、Gson和MPAndroidChart加进去implementation com.squareup.okhttp3:okhttp:4.9.3 implementation com.google.code.gson:gson:2.8.9 implementation com.github.PhilJay:MPAndroidChart:v3.1.0MPAndroidChart需要在项目级build.gradle里加JitPack仓库不然拉不下来。4. 城市查询功能的实现细节4.1 百度地图定位与反地理编码定位这块百度地图的LocationClient是核心类。初始化的时候要设置定位参数包括定位模式、返回坐标类型、是否开启缓存等。我一般用Hight_Accuracy模式兼顾精度和耗电。定位回调里拿到BDLocation对象后直接调getCity()就能拿到城市名不需要再手动调反地理编码接口。LocationClient locationClient new LocationClient(getApplicationContext()); LocationClientOption option new LocationClientOption(); option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy); option.setIsNeedAddress(true); option.setCoorType(bd09ll); locationClient.setLocOption(option); locationClient.registerLocationListener(new BDLocationListener() { Override public void onReceiveLocation(BDLocation bdLocation) { String city bdLocation.getCity(); // 拿到城市名后触发天气请求 } }); locationClient.start();注意定位回调可能多次触发要在拿到有效城市名后及时调用locationClient.stop()不然会一直回调浪费电也浪费流量。4.2 和风天气城市搜索接口调用和风天气的城市查询接口是https://geoapi.qweather.com/v2/city/lookup传参是location城市名和key。返回的JSON里有一个location数组每个元素包含name、id、adm1、adm2等字段。用户输入城市名后我把返回结果展示在一个列表里让用户选择具体是哪个城市因为同一个名字可能对应多个地方。OkHttpClient client new OkHttpClient(); String url https://geoapi.qweather.com/v2/city/lookup?location cityName key KEY; Request request new Request.Builder().url(url).build(); client.newCall(request).enqueue(new Callback() { Override public void onResponse(Call call, Response response) throws IOException { String json response.body().string(); CityResponse cityResponse new Gson().fromJson(json, CityResponse.class); // 更新UI列表 } });4.3 城市列表去重与展示逻辑城市搜索结果里经常会出现重复项比如“朝阳”既是北京的一个区也是辽宁的一个市。我的处理方式是按照adm1一级行政区和adm2二级行政区拼接一个显示名称比如“北京市-朝阳区”和“辽宁省-朝阳市”这样用户一眼就能区分。列表项点击后把对应的id存到全局变量里后续天气请求都用这个ID。实操心得和风天气的城市ID是稳定的不会变来变去所以可以放心地把它作为唯一标识。不要用城市名做标识因为重名的情况太多了。5. 天气数据拉取与解析实战5.1 实时天气接口的请求与字段映射实时天气接口是https://devapi.qweather.com/v7/weather/now传location城市ID和key。返回的now对象里有temp温度、text天气状况、humidity湿度、windDir风向等字段。我用一个NowWeather类来映射字段名不一致的地方加SerializedName。public class NowWeather { SerializedName(temp) private String temperature; SerializedName(text) private String weatherText; SerializedName(humidity) private String humidity; // 省略getter和setter }解析的时候直接new Gson().fromJson(json, NowWeather.class)就行Gson会自动把JSON字段映射到Java字段上。如果某个字段在JSON里不存在Java字段就是null不会抛异常这点比手动解析省心很多。5.2 逐日预报接口与温度数据提取逐日预报接口是https://devapi.qweather.com/v7/weather/7d返回一个daily数组每个元素包含fxDate日期、tempMax最高温、tempMin最低温、textDay白天天气等。我要做折线图所以需要把tempMax和tempMin分别抽成两个ListFloat。ListFloat maxTemps new ArrayList(); ListFloat minTemps new ArrayList(); for (Daily daily : dailyList) { maxTemps.add(Float.parseFloat(daily.getTempMax())); minTemps.add(Float.parseFloat(daily.getTempMin())); }注意和风天气返回的温度是字符串类型不是数字解析的时候要转一下。如果接口返回异常或者字段缺失parseFloat会抛NumberFormatException所以最好加个try-catch。5.3 数据缓存与请求频率控制和风天气的免费版有请求次数限制所以不能每次打开App都无脑请求。我的做法是用SharedPreferences缓存上一次的天气数据和时间戳如果距离上次请求不到30分钟就直接读缓存不再发网络请求。这样既省额度又加快了页面加载速度。long lastRequestTime sp.getLong(last_request_time, 0); if (System.currentTimeMillis() - lastRequestTime 30 * 60 * 1000) { // 读缓存 } else { // 发请求 }实操心得缓存时间不要设太长天气变化快超过半小时的数据参考价值就打折扣了。30分钟是个比较平衡的值。6. 折线图展示温度变化的完整实现6.1 MPAndroidChart初始化与坐标轴配置MPAndroidChart的LineChart初始化有几个关键配置。setDrawGridBackground(false)去掉背景网格getDescription().setEnabled(false)隐藏右下角的描述文字getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM)把X轴放到下面。Y轴这边setAxisMinimum和setAxisMaximum要根据温度数据动态计算不然折线可能贴着边缘或者超出范围。LineChart lineChart findViewById(R.id.line_chart); lineChart.getDescription().setEnabled(false); lineChart.setDrawGridBackground(false); lineChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); lineChart.getAxisRight().setEnabled(false); lineChart.getAxisLeft().setAxisMinimum(minTemp - 5); lineChart.getAxisLeft().setAxisMaximum(maxTemp 5);6.2 温度折线的数据集构建与渲染每条折线对应一个LineDataSet里面放Entry对象的列表。Entry的x值是索引y值是温度。最高温和最低温用不同颜色区分最高温用暖色最低温用冷色。setMode(LineDataSet.Mode.CUBIC_BEZIER)可以让折线变成平滑曲线看起来更舒服。ListEntry maxEntries new ArrayList(); for (int i 0; i maxTemps.size(); i) { maxEntries.add(new Entry(i, maxTemps.get(i))); } LineDataSet maxDataSet new LineDataSet(maxEntries, 最高温); maxDataSet.setColor(Color.RED); maxDataSet.setCircleColor(Color.RED); maxDataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER);X轴的标签用日期通过IndexAxisValueFormatter把索引映射成MM-dd格式的字符串。6.3 图表交互与动画效果调优MPAndroidChart自带手势缩放和拖拽但默认的缩放灵敏度有点高容易误触。我一般把setScaleEnabled(false)关掉缩放只保留拖拽。动画用animateX(800)800毫秒的横向展开动画既流畅又不拖沓。提示如果数据点比较多setDrawCircles(false)可以去掉圆点只保留折线视觉上更干净。数据点少的时候保留圆点反而更清晰。7. 常见问题与排查技巧实录7.1 定位失败与权限拒绝的处理定位失败最常见的原因是权限没给。Android 6.0以上要在运行时动态申请ACCESS_FINE_LOCATION用户拒绝之后还要给个合理的解释引导去设置页手动开启。另外如果在室内定位GPS信号弱可以降级到网络定位。百度地图的BDLocation.getLocType()返回值可以判断定位类型161是网络定位61是GPS定位62是基站定位。7.2 接口返回错误码的排查思路和风天气返回的JSON里有一个code字段200表示成功其他值都是错误。401是Key无效403是访问被拒绝404是城市ID不存在。遇到错误码先检查Key有没有拼错再检查城市ID是不是从城市查询接口拿到的。百度地图这边如果AK配置不对定位回调会返回162表示鉴权失败。7.3 折线图不显示或显示异常的修复折线图不显示先检查LineData有没有设置给LineChart再检查Entry的y值是不是在Y轴范围内。如果折线显示成一条直线可能是所有温度值都一样或者Y轴的axisMinimum和axisMaximum设成了同一个值。还有一种情况是invalidate()没调用数据更新了但图表没刷新。问题现象可能原因解决方法定位回调不触发权限未授予动态申请定位权限天气接口返回401Key无效或过期重新申请Key并更新配置折线图空白未设置LineData调用setData后invalidate城市搜索无结果城市名拼写错误检查输入并尝试拼音避坑技巧调试网络请求的时候先把URL复制到浏览器里跑一遍确认接口本身是通的再排查代码问题。这样能省很多时间。8. 源码结构与后续扩展方向8.1 项目目录组织与关键类说明源码的目录结构按功能分包ui包放Activity和Adapternetwork包放OkHttp封装和接口定义model包放Gson映射的实体类util包放权限工具和缓存工具。核心类有四个MainActivity负责整体调度LocationHelper封装定位逻辑WeatherRepository统一管理天气请求和缓存ChartManager处理折线图的配置和渲染。8.2 可以继续打磨的几个方向这个项目目前是单页面结构后续可以拆成多页面比如首页只显示实时天气第二页显示逐日预报和折线图。还可以加一个城市管理页面让用户保存多个城市左右滑动切换。数据层面可以把缓存从SharedPreferences换成Room数据库支持更复杂的查询和离线浏览。个人体会新手做项目最容易犯的错是贪多求全一开始就想把功能做全。我的建议是先跑通一条完整链路哪怕只有一个城市、只有实时天气跑通了再往上加功能。每加一个功能就测一遍确保之前的没被改坏。这样虽然看起来慢但实际比反复调试要快得多。