首先在彩云天气官网注册一个账号,注册地址是:
https://dashboard.caiyunapp.com/
访问地址接口可查询到全球绝大多数地区的数据信息
https://api.caiyunapp.com/v2/place?query=北京&token={token}&lang=zh_CN
query参数指定的是要查询的关键字(如地名),token传入刚刚申请到的令牌值。服务器会返回我们一段JSON格式的数据,我们所需获取的数据有name(该地区的名字)、location(该地区的经纬度)、formatted_address(该地区的地址)
{"status":"ok","query":"北京","places":[
{"name":"北京南站",
"formatted_address":"中国 北京市 丰台区 永外大街车站路12号",
"location":{"lat":39.865246,"lng":116.378517}},
{"name":"北京西站",
"formatted_address":"中国 北京市 丰台区 莲花池东路118号",
"location":{"lat":39.89491,"lng":116.322056}},
{"name":"北京站","formatted_address":"中国 北京市 东城区 毛家湾胡同甲13号",
"location":{"lat":39.902842,"lng":116.427341}},
{"name":"北京北站","formatted_address":"中国 北京市 西城区 北滨河路1号",
"location":{"lat":39.944876,"lng":116.353063}},
{"name":"北京东站(地铁站)","formatted_address":"中国 北京市 朝阳区 (在建)28号线",
"location":{"lat":39.902267,"lng":116.482682}}
]
}
实时天气信息API接口:
https://api.caiyunapp.com/v2.5/{token}/101.6656,39.2072/realtime
token仍是刚刚传入的令牌值,101.6656,39.2072分别是维度和经度,中间用逗号隔开,这样服务器就会把该地区的实时天气信息以JSON格式返回给我们,我们从中提取需要的数据,realtime中包含的就是当前地区的实时天气信息,其中temperature表示当前的温度,skycon表示当前的天气情况,air_quality中包含一些空气质量的数据,这里使用aqi的值作为空气质量指数显示在界面上
{
"status":"ok",
"result":{
"realtime":{
"temperature":17.0,
"skycon":"PARTLY_CLOUDY_DAY",
"air_quality":{
"aqi":{"chn":78}
}
}
}
}
未来几天的天气信息API接口
https://api.caiyunapp.com/v2.5/{token}/116.378517,39.865246/daily.json
这个接口返回的数据也比较复杂,我们依旧只需提取需要的数据
daily包含的就是当前地区未来几天的天气信息,temperature表示未来几天的温度值,skycon表示未来几天的天气情况,life_index中包含一些生活指数,coldRish表示感冒指数,CarWashing表示洗车指数,ultraviolet表示紫外线指数,dressing表示穿衣指数
{
“status:" "ok",
"result": {
"daily": {
"temperature": [ {"max":18.0,"min":9.0},...],
"skycon":[{"date":"2022-03-28T00:00+08:00","value":"PARTLY_CLOUDY_DAY"},...]
"life_index":{
"coldRisk":[{"desc":"极易发"},...],
"carWashing"[{"desc":"较不适宜"},...],
"ultraviolet":[{"desc":"强"},...],
"dressing":[{"desc:"冷""},...]
}
}
}
}
以上述中地区部分为例
{"status":"ok","query":"北京","places":[
{"name":"北京南站",
"formatted_address":"中国 北京市 丰台区 永外大街车站路12号",
"location":{"lat":39.865246,"lng":116.378517}},
{"name":"北京西站",
"formatted_address":"中国 北京市 丰台区 莲花池东路118号",
"location":{"lat":39.89491,"lng":116.322056}},
{"name":"北京站","formatted_address":"中国 北京市 东城区 毛家湾胡同甲13号",
"location":{"lat":39.902842,"lng":116.427341}},
{"name":"北京北站","formatted_address":"中国 北京市 西城区 北滨河路1号",
"location":{"lat":39.944876,"lng":116.353063}},
{"name":"北京东站(地铁站)","formatted_address":"中国 北京市 朝阳区 (在建)28号线",
"location":{"lat":39.902267,"lng":116.482682}}
]
}
我们在定义这一部分数据模型时,对每一层都需要有一个数据类,按照以上分析的JSON格式来定义
新建一个PlaceResponse.kt文件,并在这个文件中编写如下代码
/**
* 第一层:status和places的JSON数组
*/
data class PlaceResponse(val status: String, val places: List<Place>)
/**
* 第二层:name、location、formatted_address
* 由于JSON中的一些字段命名可能与kotlin的命名规范不一致,所以使用了@SerializedName注解
* @ SerializedName注解使JSON字段和kotlin字段之间建立映射关系
*/
data class Place(val name: String, val location: Location,
@SerializedName("formatted_address") val address: String)
/**
* 第三层:lng、lat
*/
data class Location(val lng: String, val lat: String)
定义好数据模型之后,我们可以开始编写网络层相关的代码了。首先定义一个用于访问彩云天气城市搜索API的Retrofit接口
还记得上面那个测试地区JSON数据的API接口吗?就是使用刚刚的接口,不过我们需要向其中传入我们的“query”和“token”以便可以通过搜索框查到大部分地区的数据
interface PlaceService {
/**
* 当调用searchPlaces时,Retrofit就会自动发起一个GET请求,去访问GET注解中配置的地址
* 其中token和lang参数都是不变的,可以直接固定写在注解中
* query参数是需要动态指定的,这里使用@Query注解的方式来实现
*
* 另外searchPlaces的返回值被声明成Call,这样JSON数据就会自动解析成PlaceResponse对象
*/
@GET("v2/place?token=${SunnyWeatherApplication.TOKEN}&lang=zh_CN")
fun searchPlaces(@Query("query") query: String) : Call<PlaceResponse>
}
现在,我们就可以开始测试进行连接通信了
新建一个Test测试类,写上主函数进行测试,需要注意的是,kotlin的主函数需要在上方加上@JvmStatic注解
class Test {
companion object{
//BASE_URL不会变,直接传入我们所需的彩云天气的URL用于指定Retrofit的根路径
private const val BASE_URL = "https://api.caiyunapp.com/"
//main函数入口
@JvmStatic
fun main(args: Array<String>) {
//从控制台输入要查询的地区名称
val placeStr = readLine()
//获取PlaceService接口的动态代理对象
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
//创建API接口对象
val placeService = retrofit.create(PlaceService::class.java)
//创建一个请求对象
val call: Call<PlaceResponse> = placeService.searchPlaces(placeStr!!)
//开始进行连接请求
call.enqueue(object : Callback<PlaceResponse> {
override fun onResponse(
call: Call<PlaceResponse>,
response: Response<PlaceResponse>
) {
val placeResponse = response.body();
if (placeResponse?.status == "ok"){
val places = response.body()?.places
for (place in places!!){
val name = place.name
val address = place.address
val location = place.location
val lng = location.lng
val lat = location.lat
println("地名:${name}, 地址:${address}, 经纬度(${lng},${lat})")
}
}
}
override fun onFailure(call: Call<PlaceResponse>, t: Throwable) {
TODO("Not yet implemented")
println("error")
}
})
}
}
}
以下就是服务器返回的数据被自动解析成JSON对象的结果
我们对数据进行进一步提取后就完成我们本次的网络请求了
上面Test类只是进行一个简单测试,在实际项目(以《第一行代码》彩云天气开发为实例进行学习)中我们不可能每次都去写一个单独的对象去获取Service接口
因此在项目中,为了更好的使用Service接口,Retrofit构建器一般会使用以下写法
新建一个ServiceCreator 单例类
object ServiceCreator {
//BASE_URL不会变,直接传入我们所需的彩云天气的URL用于指定Retrofit的根路径
private const val BASE_URL = "https://api.caiyunapp.com/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
/**
* 提供一个外部可见的create方法并接收一个class类型参数
* 这样经过封装之后,通过参数的不同可以创建相应的Service接口,而不用为每一个类都单独写一个构造器
*/
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
接下来需要定义一个统一的网络数据源访问入口,对所有的网络请求的API进行封装。
object SunnyWeatherNetwork {
//使用ServiceCreator创建一个placeService接口的动态代理对象
private val placeService = ServiceCreator.create(PlaceService::class.java)
/**
* 当外部调用SunnyWeatherNetwork的searchPlaces时,retrofit会立即发出网络请求
* 同时当前的协程也会被阻塞住,知道服务器响应我们的请求之后
* await()函数会将解析出来的数据模型对象取出并返回
*/
//定义searchPlaces函数并调用searchPlaces()方法以发起搜索城市数据请求
suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
private suspend fun <T> Call<T>.await(): T {
//suspend挂起函数关键字
//await()是一个挂起函数,给它声明一个泛型T,并将await()函数定义成call的扩展函数
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
//直接调用enqueue()方法让Retrofit发起网络请求
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
}
这样每次需要使用某个API接口时,只需要在SunnyWeatherNetwork 中创建相关的接口对象传入对应的类型参数就可以获取到了
那么现在我们对获取地区数据进行测试,其实只需要通过SunnyWeatherNetwork.searchPlaces()就能得到地区数据了
因为searchPlaces()被设置为suspend挂起,因此给刚刚的main()加上一个runBlocking(调用了 runblocking 的线程会阻塞直到内部的协程执行完毕)这样就可以执行了
companion object{
@JvmStatic
fun main(args: Array<String>) = runBlocking{
//从控制台输入要查询的地区名称
val placeStr = readLine()
/**
* 在SunnyWeatherNetwork中已经创建了placeService接口的动态代理对象
* 若要使用别的接口,也只需要在SunnyWeatherNetwork添加方法即可
*/
val placeResponse = SunnyWeatherNetwork.searchPlaces(placeStr!!)
if (placeResponse.status == "ok"){
val places = placeResponse.places
for (place in places){
val name = place.name
val address = place.address
val location = place.location
val lng = location.lng
val lat = location.lat
println("地名:${name}, 地址:${address}, 经纬度(${lng},${lat})")
}
}
}
}
其实现在真正发送请求只需要一行代码:
val placeResponse = SunnyWeatherNetwork.searchPlaces(placeStr!!)
而且现在调用其他的API接口都可以按照这种模式进行编写