彩云天气JSON数据解析

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

彩云天气API

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

https://dashboard.caiyunapp.com/

注册之后可查看API文档
彩云天气JSON数据解析_第1张图片

天气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张图片

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张图片

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数据解析_第4张图片

使用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,这样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对象的结果
彩云天气JSON数据解析_第5张图片
我们对数据进行进一步提取后就完成我们本次的网络请求了
彩云天气JSON数据解析_第6张图片
上面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接口都可以按照这种模式进行编写

你可能感兴趣的:(json)