JSON数据解析——彩云天气api

彩云天气API

首先在彩云天气官网注册一个账号,注册地址是:

https://dashboard.caiyunapp.com/

注册之后可查看API文档
彩云天气JSON数据解析-编程之家

天气app所需数据

1、地区数据

访问地址接口可查询到全球绝大多数地区的数据信息

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}}]
}

其展示效果如下:
彩云天气JSON数据解析-编程之家

2、实时天气数据

实时天气信息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}}}}
}

其展示效果如下:
彩云天气JSON数据解析-编程之家

3、未来几天的天气数据

未来几天的天气信息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:"冷""},...]}}}
}

其展示效果如下:
彩云天气JSON数据解析-编程之家

使用retrofit请求api获取数据

以上述中地区部分为例

{"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数据:

  • 第一层是一个花括号,即jsonObject对象,其中有status、query属性以及一个places的JSON数组(中括号为JSONArray数组)
  • 第二层places的JSON数组,其中有name、formatted_address、location
  • 第三层location有lat和lng两个属性

kotlin代码实现请求数据

我们在定义这一部分数据模型时,对每一层都需要有一个数据类,按照以上分析的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<PlaceResponse>,这样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函数入口@JvmStaticfun 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()?.placesfor (place in places!!){val name = place.nameval address = place.addressval location = place.locationval lng = location.lngval lat = location.latprintln("地名:${name},  地址:${address},  经纬度(${lng},${lat})")}}}override fun onFailure(call: Call<PlaceResponse>, t: Throwable) {TODO("Not yet implemented")println("error")}})}}
}

以下就是服务器返回的数据被自动解析成JSON对象的结果
彩云天气JSON数据解析-编程之家
我们对数据进行进一步提取后就完成我们本次的网络请求了
彩云天气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<T>的扩展函数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{@JvmStaticfun main(args: Array<String>) = runBlocking{//从控制台输入要查询的地区名称val placeStr = readLine()/*** 在SunnyWeatherNetwork中已经创建了placeService接口的动态代理对象* 若要使用别的接口,也只需要在SunnyWeatherNetwork添加方法即可*/val placeResponse = SunnyWeatherNetwork.searchPlaces(placeStr!!)if (placeResponse.status == "ok"){val places = placeResponse.placesfor (place in places){val name = place.nameval address = place.addressval location = place.locationval lng = location.lngval lat = location.latprintln("地名:${name},  地址:${address},  经纬度(${lng},${lat})")}}}
}

其实现在真正发送请求只需要一行代码:

val placeResponse = SunnyWeatherNetwork.searchPlaces(placeStr!!)

而且现在调用其他的API接口都可以按照这种模式进行编写