Android Retrofit的学习

Android Retrofit的学习

关于 《第一行代码:android(第三版)》中的天气预报APP的学习记录
基于 MVVM 架构
基于 kotlin
涉及到了协程,高级函数,Lambda

导入依赖

dependencies{
	implementation 'com.squareup.retrofit2:retrofit:2.6.1'
	//使用Gson作为json转换器
    implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
    //用于kotlin协程的支持
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}

具体步骤

  1. 定义数据模型
  2. 定义访问API的Retrofit接口
  3. 定义Retrofit构建器
  4. 定义网络数据源访问入口类
  5. 定义仓库类

1. 数据模型

使用 Kotlin提供的data class关键字创建数据类,提供了:

  • 自动生成属性的 equals()hashCode()toString() 方法:数据类会自动从主构造函数中定义的属性生成适当的 equals()hashCode()toString() 方法。这使得你可以简化比较对象、检查对象的哈希值,以及方便地显示对象的字符串表示。
  • 自动拷贝函数:通过在数据类声明中添加 copy() 方法,可以轻松地创建一个与原始对象属性值相同的新对象实例。这对于创建对象的修改副本非常方便。
  • 自动解构声明:数据类的主构造函数中的属性可以被自动解构,使得你可以直接通过声明变量来获取属性值。这使得从数据类中获取属性值变得更加便捷。
  • 属性的访问器方法:数据类会自动为每个属性生成默认的 getset 方法。你可以直接访问和修改属性的值。
  • component1()component2() 等方法:数据类允许通过 componentN() 方法按照属性在主构造函数中的声明顺序分解对象。这对于在 Lambda 表达式、解构声明和集合操作中使用非常方便。
/**
 * 在 Kotlin 中定义数据模型时,可以使用 @SerializedName 注解来指定 JSON 字段的名称,从而解决 JSON 字段命名和 Kotlin 命名规范不一致的问题。
 * @SerializedName 注解是在 Gson 序列化和反序列化时使用的,它可以把 JSON 数据中的字段名映射为 Kotlin 中的属性名。
 */
data class PlaceResponse(val status:String,val places:List<Place>)
data class Place(val name: String, val location:Location,
                 @SerializedName("formatted_address") val address:String)
data class Location(val lng:String,val lat:String)

2. Retrofit接口

Retrofit接口:定义一个 API 接口,并通过注解来指定请求的 HTTP 方法、URL 和参数。

interface PlaceService {
    /**
     * 面声明了一个@GET注解,这样当调用searchPlaces()方法的时候,Retrofit就会自动发起一条GET请求,去访问@GET注解中配置的地址。
     * 其中,搜索城市数据的API中只有query这个参数是需要动态指定的,我们使用@Query注解的方式来进行实现,另外两个参数是不会变的,因此固定写在@GET注解中即可。
     * searchPlaces()方法的返回值被声明成了Call<PlaceResponse>,这样Retrofit就会将服务器返回的JSON数据自动解析成PlaceResponse对象了。
     */
    @GET("v2/place?token=${SunnyWeatherApplication.TOKEN}&lang=zh_CN")
    fun searchPlaces(@Query("query") query: String): Call<PlaceResponse>

}

3. Retrofit构建器

Retrofit构建器:用来创建 Retrofit 实例的工具。
构建器允许你配置 Retrofit 的各种属性,例如 API 的基本 URL、解析器和拦截器等。
创建一个 Retrofit 实例,使用了 Retrofit.Builder() 来初始化构建器。然后通过方法链形式,配置属性。

object ServiceCreator {
	//api地址
    private const val BASE_URL = "https://api.caiyunapp.com/"
    /**
     * 通过 Retrofit.Builder() 创建了一个 Retrofit 实例,并使用 .baseUrl() 方法指定了 API 的基本 URL。
     * 然后,通过 .addConverterFactory() 方法添加了一个 JSON 解析器(这里使用 Gson)来处理响应结果。
     * 最后,使用 .build() 方法创建了 Retrofit 实例。
     * 通过 Retrofit 实例的 .create() 方法,可以创建指定接口的实例,用于发送请求。
     * 用private修饰符来声明的,相当于对于外部而言它们都是不可见的。
     */
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    /**
     * 提供一个外部可见的create()方法,并接收一个Class类型的参数。
     * 当在外部调用这个方法时,实际上就是调用了Retrofit对象的create()方法,从而创建出相应Service接口的动态代理对象。
     * 这个函数接受一个 Class<T> 类型的参数 serviceClass,其中 T 是服务接口的类型。
     * 它使用 Retrofit 实例的 create() 方法来创建一个对应于指定服务接口的服务对象。然后将该服务对象返回。
     */
    fun <T> create(serviceClass: Class<T>):T = retrofit.create(serviceClass)

    /**
     * 这个函数使用了使用inline关键字来修饰方法, Kotlin 的 reified 类型参数(泛型实化),它可以让我们在运行时获取泛型的实际类型。
     * 它没有接受任何参数,而是利用 reified 类型参数 T 来获取服务接口的类型。
     * 然后它调用第一个函数,将获取到的类型参数传递给它,并返回生成的服务对象。
     */
    inline fun <reified T>create():T = create(T::class.java)
}

4. 网络数据源访问入口累

统一的网络数据源访问入口,对所有网络请求的API进行封装。

object SunnyWeatherNetwork {
    /**
     * 使用ServiceCreator创建了一个PlaceService接口的动态代理对象
     */
    private val placeService=ServiceCreator.create<PlaceService>()

    /**
     * 1.定义一个searchPlaces()函数,并在这里调用刚刚在PlaceService接口中定义的searchPlaces()方法,以发起搜索城市数据请求。
     * 2.为了让代码变得更加简洁,来简化Retrofit回调的写法。
     * 3.由于是需要借助协程技术来实现的,因此这里又定义了一个await()函数,并将searchPlaces()函数也声明成挂起函数。
     * 4.当外部调用SunnyWeatherNetwork的searchPlaces()函数时,Retrofit就会立即发起网络请求,同时当前的协程也会被阻塞住。
     直到服务器响应我们的请求之后,await()函数会将解析出来的数据模型对象取出并返回,同时恢复当前协程的执行,searchPlaces()函数在得到await()函数的返回值后会将该数据再返回到上一层。
     */
    suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
    private suspend fun <T> Call<T>.await(): T {
        return suspendCoroutine { continuation ->
            enqueue(object : Callback<T> {
                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)
                }
            })
        }
    }
}

5. 仓库类

仓库类:是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获得的数据返回给调用方。
仓库类有点像是一个数据获取与缓存的中间层,在本地没有缓存数据的情况下就去网络层获取,如果本地已经有缓存了,就直接将缓存数据返回。

object Repository {
    /**
     * 一般在仓库层中定义的方法,为了能将异步获取的数据以响应式编程的方式通知给上一层,通常会返回一个LiveData 对象。
     * liveData()函数是lifecycle-livedata-ktx库提供的一个非常强大且好用的功能,它可以自动构建并返回一个LiveData对象,
     然后在它的代码块中提供一个挂起函数的上下文,这样我们就可以在liveData()函数的代码块中调用任意的挂起函数了。
     * 调用SunnyWeatherNetwork的searchPlaces()函数来搜索城市数据,
     * 然后判断如果服务器响应的状态是ok,那么就使用Kotlin 内置的Result.success()方法来包装获取的城市数据列表,
     否则使用Result.failure()方法来包装一个异常信息。
     * 最后使用一个emit()方法将包装的结果发射出去,这个emit()方法其实类似于调用LiveData的setValue()方法来通知数据变化,
     只不过这里我们无法直接取得返回的LiveData 对象,所以lifecycle-livedata-ktx 库提供了这样一个替代方法。
     * liveData()函数的线程参数类型指定成了Dispatchers.IO,这样代码块中的所有代码就都运行在子线程中了。
     * 众所周知,Android是不允许在主线程中进行网络请求的,诸如读写数据库之类的本地数据操作也是不建议在主线程
    中进行的,因此非常有必要在仓库层进行一次线程转换。
     */
    /*
    fun searchPlaces(query: String) = liveData(Dispatchers.IO) {
        val result = try {
            val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
            if (placeResponse.status == "ok") {
                val places = placeResponse.places
                Result.success(places)
            } else {
                Result.failure(RuntimeException("response status is
                    ${placeResponse.status}"))
            }
        } catch (e: Exception) {
            Result.failure<List<Place>>(e)
        }
        emit(result)
    }
    */
    /**
     * refreshWeather()方法用来刷新天气信息。因为对于调用方而言,需要调用两次
    请求才能获得其想要的所有天气数据明显是比较烦琐的行为,因此最好的做法就是在仓库层再
    进行一次统一的封装。
     * 获取实时天气信息和获取未来天气信息这两个请求是没有先后顺序的,因此让它们并发
    执行可以提升程序的运行效率
     * 分别在两个async函数中发起网络请求,然后再分别调用它们的await()方法,
    就可以保证只有在两个网络请求都成功响应之后,才会进一步执行程序。另外,由于async函数必须在协程作用域内才能调用,
    所以这里又使用coroutineScope函数创建了一个协程作用域。
     * 在同时获取到RealtimeResponse和DailyResponse之后,如果它们的响应状态都是ok,
    那么就将Realtime和Daily对象取出并封装到一个Weather对象中,然后使用Result.success()方法来包装这个Weather对象,
    否则就使用Result.failure()方法来包装一个异常信息,最后调用emit()方法将包装的结果发射出去。
     */
    /*
    fun refreshWeather(lng: String, lat: String) = liveData(Dispatchers.IO) {
        val result = try {
            coroutineScope {
                val deferredRealtime = async {
                    SunnyWeatherNetwork.getRealtimeWeather(lng, lat)
                }
                val deferredDaily = async {
                    SunnyWeatherNetwork.getDailyWeather(lng, lat)
                }
                val realtimeResponse = deferredRealtime.await()
                val dailyResponse = deferredDaily.await()
                if (realtimeResponse.status == "ok" && dailyResponse.status == "ok") {
                    val weather = Weather(realtimeResponse.result.realtime,
                        dailyResponse.result.daily)
                    Result.success(weather)
                } else {
                    Result.failure(
                        RuntimeException(
                            "realtime response status is ${realtimeResponse.status}" +
                                    "daily response status is ${dailyResponse.status}"
                        )
                    )
                }
            }
        } catch (e: Exception) {
            Result.failure<Weather>(e)
        }
        emit(result)
    }
    */


    /**
     *由于使用了协程来简化网络回调的写法,导致SunnyWeatherNetwork中封装的每个网络请求接口都可
    能会抛出异常,于是我们必须在仓库层中为每个网络请求都进行try catch处理,这无疑增加了
    仓库层代码实现的复杂度。其实完全可以在某个统一的入口函数中进行封装,使得只要进行一次try catch处理就行了,
     */
    fun searchPlaces(query: String) = fire(Dispatchers.IO){
        val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
        if (placeResponse.status == "ok") {
            val places = placeResponse.places
            Result.success(places)
        } else {
            Result.failure(RuntimeException("response status is ${placeResponse.status}"))
        }
    }
    fun refreshWeather(lng: String, lat: String) =  fire(Dispatchers.IO) {
        coroutineScope {
            val deferredRealtime = async {
                SunnyWeatherNetwork.getRealtimeWeather(lng, lat)
            }
            val deferredDaily = async {
                SunnyWeatherNetwork.getDailyWeather(lng, lat)
            }
            val realtimeResponse = deferredRealtime.await()
            val dailyResponse = deferredDaily.await()
            if (realtimeResponse.status == "ok" && dailyResponse.status == "ok") {
                val weather = Weather(realtimeResponse.result.realtime,
                    dailyResponse.result.daily)
                Result.success(weather)
            } else {
                Result.failure(
                    RuntimeException(
                        "realtime response status is ${realtimeResponse.status}" +
                                "daily response status is ${dailyResponse.status}"
                    )
                )
            }
        }
    }

    /**
     * fire()函数,这是一个按照liveData()函数的参数
    接收标准定义的一个高阶函数。在fire()函数的内部会先调用一下liveData()函数,然后在
    liveData()函数的代码块中统一进行了try catch 处理,并在try语句中调用传入的Lambda
    表达式中的代码,最终获取Lambda 表达式的执行结果并调用emit()方法发射出去。
     * 在liveData()函数的代码块中,我们是拥有挂起函数上下文的,可是当回调到Lambda表达式中,
    代码就没有挂起函数上下文了,但实际上Lambda表达式中的代码一定也是在挂起函数中运行的。
    为了解决这个问题,我们需要在函数类型前声明一个suspend关键字,以表示所有传入的Lambda表达式中的代码也是拥有挂起函数上下文的。
     */
    private fun <T> fire(context: CoroutineContext, block: suspend () -> Result<T>) =
        liveData<Result<T>>(context) {
            val result = try {
                block()
            } catch (e: Exception) {
                Result.failure<T>(e)
            }
            emit(result)
        }
    fun savePlace(place: Place) = PlaceDao.savePlace(place)
    fun getSavedPlace() = PlaceDao.getSavedPlace()
    fun isPlaceSaved() = PlaceDao.isPlaceSaved()
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值