一、引言
上个月我通过腾讯位置服务,实现了手机定位应用的开发学习。最近在看软件书籍时,又看到了聚合数据API方面的内容。 书上介绍了聚合数据天气预报API的应用,不过书上的代码看得有些难受,我到聚合数据官网,对天气预报API的接口文档进行了研究,感觉比书上的要简单。于是,我参照官网的接口文档设计查询部分的代码,UI等设计则借鉴了书上的内容,完成了这个应用的开发。实现的效果如下图:
二、聚合数据平台选择API
聚合数据平台提供了很多的API,其中免费的API也不少。要使用该平台的API自然需要先注册用户了。
注册号用户,登录后,先完成实名认证,否则无法使用API。我是后知后觉,申请API的时候出现了要求实名认证的提示。
大体的流程在网站上有说明。
完成注册和认证后,进入API页面,选择免费接口 (免费API接口工具大全 - 聚合数据)。我们要用到的天气预报接口就在第一行。
点击天气预报,进入天气预报接口页面。点击“立即申请”。这个页面里还可以获得接口的相关介绍和示例代码,方便我们进行应用开发。
完成申请后就可以在“个人中心 - 数据中心 - 我的API”中看到申请到的API了。聚合数据对免费接口有限制,普通会员只能申请3个。大多数免费接口每天的请求次数为50次,进行开发学习还是够用了。调用API需要的Key也在这个页面里。
完成了API的申请,就可以着手进行软件的设计开发了。
三、软件设计
我的天气预报应用的界面设计参考了书上的样式,但书上的代码将城市名写死了,我希望支持输入城市名进行查询。因此设计UI时,在页面顶部添加了SearchView组件,用于输入城市名称,输入后按下软键盘中的搜索按钮,执行获取天气预报操作。界面的中间部分放置了几个TextView组件用于显示实时天气信息,界面下部使用纵向的LinearLayout布局设置,该布局做了圆角处理,其下放置了多个TextView组件用于显示未来5天的预报信息。有参照做起来就很快了。
逻辑代码的设计则花了一些时间,主要是书上的代码看着不够简洁,比官网提供的示例要复杂不少。我就没有照搬书上的内容做,而是综合了书上的代码与官网示例进行的设计。其中GET请求部分的代码基本照搬了“接口文档”中提供的Java示例。有所区别的是,需要添加try语句,否则会报错。
官网提供了接口测试功能,可以先在接口测试页面查看获取天气预报的请求详情和返回结果。方便后续的代码设计。
这个API的调用接口URI格式如下:
http://apis.juhe.cn/simpleWeather/query?key=key&city=%E8%8B%8F%E5%B7%9E
其中key在“个人中心 - 数据中心 - 我的API”中获取。city则是从SearchView组件中获取。我将GET请求放在了SearchView组件的监听器中执行。监听器响应搜索按钮触发,获取组件中的文本内容,并将其传递给获取天气预报的方法。
获取天气预报的方法,我是参考了官网的java示例代码(如下):
package cn.juhe.test;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class JavaGet {
public static void main(String[] args) throws Exception {
String apiKey = "你申请的key";
String apiUrl = "http://apis.juhe.cn/simpleWeather/query";
HashMap<String, String> map = new HashMap<>();
map.put("key", apiKey);
map.put("city", "苏州");
URL url = new URL(String.format(apiUrl + "?" + params(map)));
BufferedReader in = new BufferedReader(new InputStreamReader((url.openConnection()).getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println(response);
}
public static String params(Map<String, String> map) {
return map.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
}
}
上述代码也是将城市名写死的,且获取到的结果直接打印输出,不符合我们的实际应用需求,得修改,但关键的代码基本是可以照搬的。
在做逻辑代码设计时,我遇到了两个问题:
1.一开始,我的GET请求代码是放在主线程中执行的,结果测试时出现了报错。网上搜索后,才了解到从Android 4.0 之后不能在主线程中请求HTTP,需要将GET请求放在分线程中执行。
2.在分线程中执行GET请求获取到天气预报的信息后,我在分线程里更新UI的代码,结果测试时又报错了。一搜,Android不允许在分线程中直接修改UI界面,可以使用runOnUiThread方法更新UI界面。
解决了这两个问题,其它方面就很顺利了,完成后测试了几次,还可以。
请求次数可以在“个人中心-数据中心-我的API”找到天气预报,点击“统计”按钮,可以查看调用情况。
四、代码展示
最终的代码如下:
1. 界面设计文件 activity_weather.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".WeatherActivity">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="天气预报"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="10dp"
android:text="城市"
android:textSize="17sp" />
<android.widget.SearchView
android:id="@+id/sv_cityName"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/shape_round_bg_gray"
android:iconifiedByDefault="true"
android:imeOptions="actionSearch"
android:queryHint="请输入关键字"
android:textColor="@color/gray_78"
android:textSize="15sp" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:background="@color/blue_415"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_cityName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="城市名"
android:textColor="@color/white"
android:textSize="24sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_realTime_temp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="~℃"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_realTime_weather"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="天气"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_realTime_dir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="风向"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_realTime_pow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="风级"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_realTime_hum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="湿度~"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_realTime_aqi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="空气质量指数~"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/radius_border_15"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="5天预报"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_date1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="日期1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_temp1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="~℃" />
<TextView
android:id="@+id/tv_weather1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="天气" />
<TextView
android:id="@+id/tv_dir1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="风向" />
</LinearLayout>
<TextView
android:id="@+id/tv_date2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="日期2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_temp2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="~℃" />
<TextView
android:id="@+id/tv_weather2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="天气" />
<TextView
android:id="@+id/tv_dir2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="风向" />
</LinearLayout>
<TextView
android:id="@+id/tv_date3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="日期3" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_temp3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="~℃" />
<TextView
android:id="@+id/tv_weather3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="天气" />
<TextView
android:id="@+id/tv_dir3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="风向" />
</LinearLayout>
<TextView
android:id="@+id/tv_date4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="日期4" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_temp4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="~℃" />
<TextView
android:id="@+id/tv_weather4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="天气" />
<TextView
android:id="@+id/tv_dir4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="风向" />
</LinearLayout>
<TextView
android:id="@+id/tv_date5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="日期5" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_temp5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="~℃" />
<TextView
android:id="@+id/tv_weather5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="天气" />
<TextView
android:id="@+id/tv_dir5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="风向" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
2. 逻辑代码 WeatherActivity.java
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.SearchView;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; // 与官网示例不同,官网是net.sf.json.JSONObject
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
public class WeatherActivity extends AppCompatActivity {
private final static String TAG = "WeatherActivity";
private SearchView sv_cityName; // 搜索框
private TextView tv_cityName; // 显示城市名
private TextView tv_realTime_temp; // 显示实时温度
private TextView tv_realTime_weather; // 显示实时天气
private TextView tv_realTime_dir; // 显示实时风向
private TextView tv_realTime_pow; // 显示实时风级
private TextView tv_realTime_hum; // 显示实时湿度
private TextView tv_realTime_aqi; // 显示空气质量指数
private TextView tv_date1, tv_date2, tv_date3, tv_date4, tv_date5; // 日期
private TextView tv_temp1, tv_temp2, tv_temp3, tv_temp4, tv_temp5; // 温度
private TextView tv_weather1, tv_weather2, tv_weather3, tv_weather4, tv_weather5; // 天气
private TextView tv_dir1, tv_dir2, tv_dir3, tv_dir4, tv_dir5; // 风向
private String mCityName; // 保存用户在搜索框中输入的城市名
// 天气情况查询接口地址
private static final String API_URL = "http://apis.juhe.cn/simpleWeather/query";
// 接口请求Key(在聚合数据网站申请天气预报API后生成的AppKey)
private static final String API_KEY = "59e7634610d0c07c8e4aa4a5d1dee643";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_weather);
sv_cityName = findViewById(R.id.sv_cityName); // 城市名搜索框
// 实时天气
tv_cityName = findViewById(R.id.tv_cityName);
tv_realTime_temp = findViewById(R.id.tv_realTime_temp);
tv_realTime_weather = findViewById(R.id.tv_realTime_weather);
tv_realTime_dir = findViewById(R.id.tv_realTime_dir);
tv_realTime_pow = findViewById(R.id.tv_realTime_pow);
tv_realTime_hum = findViewById(R.id.tv_realTime_hum);
tv_realTime_aqi = findViewById(R.id.tv_realTime_aqi);
// 未来5天预报天气
tv_date1 = findViewById(R.id.tv_date1);
tv_date2 = findViewById(R.id.tv_date2);
tv_date3 = findViewById(R.id.tv_date3);
tv_date4 = findViewById(R.id.tv_date4);
tv_date5 = findViewById(R.id.tv_date5);
tv_temp1 = findViewById(R.id.tv_temp1);
tv_temp2 = findViewById(R.id.tv_temp2);
tv_temp3 = findViewById(R.id.tv_temp3);
tv_temp4 = findViewById(R.id.tv_temp4);
tv_temp5 = findViewById(R.id.tv_temp5);
tv_weather1 = findViewById(R.id.tv_weather1);
tv_weather2 = findViewById(R.id.tv_weather2);
tv_weather3 = findViewById(R.id.tv_weather3);
tv_weather4 = findViewById(R.id.tv_weather4);
tv_weather5 = findViewById(R.id.tv_weather5);
tv_dir1 = findViewById(R.id.tv_dir1);
tv_dir2 = findViewById(R.id.tv_dir2);
tv_dir3 = findViewById(R.id.tv_dir3);
tv_dir4 = findViewById(R.id.tv_dir4);
tv_dir5 = findViewById(R.id.tv_dir5);
// 设置搜索框监听器
sv_cityName.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
// 当点击搜索按钮时触发该方法
@Override
public boolean onQueryTextSubmit(String s) {
sv_cityName.clearFocus(); // 移除焦点
mCityName = s;
// Android 4.0 之后不能在主线程中请求HTTP
new Thread(() -> queryWeather (mCityName)).start(); // 分线程中获取天气信息
return false;
}
// 当搜索内容改变时触发该方法
@Override
public boolean onQueryTextChange(String s) {
return false;
}
});
}
/**
* 根据城市名查询天气情况
*
* @param cityName 城市名称
*/
private void queryWeather(String cityName) {
HashMap<String, String> map = new HashMap<>(); //组合参数
map.put("city", cityName);
map.put("key", API_KEY);
String queryParams = params(map);
try {
URL url = new URL(API_URL + "?" + queryParams);
Log.d(TAG, "URL=" + url);
BufferedReader in = new BufferedReader(new InputStreamReader((url.openConnection()).getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer(); // StringBuffer是线程安全的,StringBuilder效率更高,但不是线程安全的
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Log.d(TAG, "查询天气返回的结果:");
// Log.d(TAG, response.toString());
// 将获取到的结果转换为JSONObject,从中获取天气信息
try {
JSONObject jsonObject = new JSONObject(response.toString());
int error_code = jsonObject.getInt("error_code");
if (error_code == 0) {
JSONObject result = jsonObject.getJSONObject("result");
String city = result.getString("city");
// 获取实时天气数据
JSONObject realtime = result.getJSONObject("realtime");
String temp = realtime.getString("temperature"); // 温度
String hum = realtime.getString("humidity"); // 湿度
String info = realtime.getString("info"); // 天气
String dir = realtime.getString("direct"); // 风向
String pow = realtime.getString("power"); // 风级
String aqi = realtime.getString("power"); // 空气质量指数
// 获取未来5天的天气数据
JSONArray futureArray = result.getJSONArray("future");
JSONObject f1 = futureArray.getJSONObject(0);
String date1 = f1.getString("date");
String temp1 = f1.getString("temperature");
String weather1 = f1.getString("weather");
String dir1 = f1.getString("direct");
JSONObject f2 = futureArray.getJSONObject(1);
String date2 = f2.getString("date");
String temp2 = f2.getString("temperature");
String weather2 = f2.getString("weather");
String dir2 = f2.getString("direct");
JSONObject f3 = futureArray.getJSONObject(2);
String date3 = f3.getString("date");
String temp3 = f3.getString("temperature");
String weather3 = f3.getString("weather");
String dir3 = f3.getString("direct");
JSONObject f4 = futureArray.getJSONObject(3);
String date4 = f4.getString("date");
String temp4 = f4.getString("temperature");
String weather4 = f4.getString("weather");
String dir4 = f4.getString("direct");
JSONObject f5 = futureArray.getJSONObject(4);
String date5 = f5.getString("date");
String temp5 = f5.getString("temperature");
String weather5 = f5.getString("weather");
String dir5 = f5.getString("direct");
// 分线程不能直接修改UI界面,可以使用runOnUiThread方法更新UI界面
runOnUiThread(() -> {
tv_cityName.setText(city);
// 更新实时天气
tv_realTime_temp.setText(String.format(Locale.CHINESE, "%s%s", temp, "℃"));
tv_realTime_weather.setText(info);
tv_realTime_dir.setText(dir);
tv_realTime_pow.setText(pow);
tv_realTime_hum.setText(String.format(Locale.CHINESE, "%s%s", "湿度:", hum));
tv_realTime_aqi.setText(String.format(Locale.CHINESE, "%s%s", "空气质量指数:", aqi));
// 更新未来5天预报天气
tv_date1.setText(date1);
tv_temp1.setText(temp1);
tv_weather1.setText(weather1);
tv_dir1.setText(dir1);
tv_date2.setText(date2);
tv_temp2.setText(temp2);
tv_weather2.setText(weather2);
tv_dir2.setText(dir2);
tv_date3.setText(date3);
tv_temp3.setText(temp3);
tv_weather3.setText(weather3);
tv_dir3.setText(dir3);
tv_date4.setText(date4);
tv_temp4.setText(temp4);
tv_weather4.setText(weather4);
tv_dir4.setText(dir4);
tv_date5.setText(date5);
tv_temp5.setText(temp5);
tv_weather5.setText(weather5);
tv_dir5.setText(dir5);
}); // 使用runOnUiThread更新界面
} else {
Log.d(TAG, "调用接口失败:" + jsonObject.getString("reason"));
}
} catch (JSONException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将map型转为请求参数型
*
* @param map map型保存的参数
*/
private static String params(Map<String, String> map) {
return map.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
}
}