如何在 Kotlin 中将 Fuel 与协程一起使用?
How to use Fuel with coroutines in Kotlin?
我想获得一个 API 请求并将请求的数据保存到数据库中。还要return的数据(即写入DB)。我知道,这在 RxJava 中是可能的,但现在我在 Kotlin 协程中编写,目前使用 Fuel 而不是 Retrofit(但差异不是那么大)。我看了,但没看懂
怎么写协程和方法?
更新
说,我们有一个 Java 和 Retrofit,RxJava。那我们就可以写一段代码了。
区域响应:
@AutoValue
public abstract class RegionResponse {
@SerializedName("id")
public abstract Integer id;
@SerializedName("name")
public abstract String name;
@SerializedName("countryId")
public abstract Integer countryId();
public static RegionResponse create(int id, String name, int countryId) {
....
}
...
}
地区:
data class Region(
val id: Int,
val name: String,
val countryId: Int)
网络:
public Single<List<RegionResponse>> getRegions() {
return api.getRegions();
// @GET("/regions")
// Single<List<RegionResponse>> getRegions();
}
区域存储库:
fun getRegion(countryId: Int): Single<Region> {
val dbSource = db.getRegion(countryId)
val lazyApiSource = Single.defer { api.regions }
.flattenAsFlowable { it }
.map { apiMapper.map(it) }
.toList()
.doOnSuccess { db.updateRegions(it) }
.flattenAsFlowable { it }
.filter({ it.countryId == countryId })
.singleOrError()
return dbSource
.map { dbMapper.map(it) }
.switchIfEmpty(lazyApiSource)
}
区域交互器:
class RegionInteractor(
private val repo: RegionRepository,
private val prefsRepository: PrefsRepository) {
fun getRegion(): Single<Region> {
return Single.fromCallable { prefsRepository.countryId }
.flatMap { repo.getRegion(it) }
.subscribeOn(Schedulers.io())
}
}
我们一层一层的来看
首先,据我所知,您的 RegionResponse
和 Region
对于这个用例来说完全没问题,所以我们根本不会碰它们。
您的网络层是用 Java 编写的,因此我们假设它始终需要同步行为,并且也不会触及它。
所以,我们从回购开始:
fun getRegion(countryId: Int) = async {
val regionFromDb = db.getRegion(countryId)
if (regionFromDb == null) {
return apiMapper.map(api.regions).
filter({ it.countryId == countryId }).
first().
also {
db.updateRegions(it)
}
}
return dbMapper.map(regionFromDb)
}
请记住,我没有您的代码,因此细节可能会有所不同。但是协程的一般想法是,如果他们需要 return 结果,你用 async()
启动它们,然后编写你的代码,就好像你在不需要的完美世界中一样关注并发。
现在是交互者:
class RegionInteractor(
private val repo: RegionRepository,
private val prefsRepository: PrefsRepository) {
fun getRegion() = withContext(Schedulers.io().asCoroutineDispatcher()) {
val countryId = prefsRepository.countryId
return repo.getRegion(countryId).await()
}
}
您需要一些东西来将异步代码转换回同步代码。为此,您需要某种线程池来执行。这里我们使用 Rx 的线程池,但如果你想使用其他池,也可以。
研究了, Fuel coroutines and https://github.com/kittinunf/Fuel/(找了awaitStringResponse
),又做了一个解决方案。假设您有 Kotlin 1.3 以及协程 1.0.0 和 Fuel 1.16.0。
我们必须避免带有回调的异步请求并进行同步(协程中的每个请求)。比如说,我们想通过代码显示国家名称。
// POST-request to a server with country id.
fun getCountry(countryId: Int): Request =
"map/country/"
.httpPost(listOf("country_id" to countryId))
.addJsonHeader()
// Adding headers to the request, if needed.
private fun Request.addJsonHeader(): Request =
header("Content-Type" to "application/json",
"Accept" to "application/json")
它给出了 JSON:
{
"country": {
"name": "France"
}
}
要解码 JSON 响应,我们必须编写一个模型 class:
data class CountryResponse(
val country: Country,
val errors: ErrorsResponse?
) {
data class Country(
val name: String
)
// If the server prints errors.
data class ErrorsResponse(val message: String?)
// Needed for awaitObjectResponse, awaitObject, etc.
class Deserializer : ResponseDeserializable<CountryResponse> {
override fun deserialize(content: String) =
Gson().fromJson(content, CountryResponse::class.java)
}
}
那么我们应该创建一个UseCase或者Interactor来同步接收一个结果:
suspend fun getCountry(countryId: Int): Result<CountryResponse, FuelError> =
api.getCountry(countryId).awaitObjectResponse(CountryResponse.Deserializer()).third
我使用 third
访问响应数据。但是,如果您希望检查 HTTP 错误代码 != 200,请删除 third
并稍后获取所有三个变量(作为 Triple
变量)。
现在你可以写一个打印国家名称的方法了。
private fun showLocation(
useCase: UseCaseImpl,
countryId: Int,
regionId: Int,
cityId: Int
) {
GlobalScope.launch(Dispatchers.IO) {
// Titles of country, region, city.
var country: String? = null
var region: String? = null
var city: String? = null
val countryTask = GlobalScope.async {
val result = useCase.getCountry(countryId)
// Receive a name of the country if it exists.
result.fold({ response -> country = response.country.name }
, { fuelError -> fuelError.message })
}
}
val regionTask = GlobalScope.async {
val result = useCase.getRegion(regionId)
result.fold({ response -> region = response.region?.name }
, { fuelError -> fuelError.message })
}
val cityTask = GlobalScope.async {
val result = useCase.getCity(cityId)
result.fold({ response -> city = response.city?.name }
, { fuelError -> fuelError.message })
}
// Wait for three requests to execute.
countryTask.await()
regionTask.await()
cityTask.await()
// Now update UI.
GlobalScope.launch(Dispatchers.Main) {
updateLocation(country, region, city)
}
}
}
在build.gradle
中:
ext {
fuelVersion = "1.16.0"
}
dependencies {
...
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
// Fuel.
//for JVM
implementation "com.github.kittinunf.fuel:fuel:${fuelVersion}"
//for Android
implementation "com.github.kittinunf.fuel:fuel-android:${fuelVersion}"
//for Gson support
implementation "com.github.kittinunf.fuel:fuel-gson:${fuelVersion}"
//for Coroutines
implementation "com.github.kittinunf.fuel:fuel-coroutines:${fuelVersion}"
// Gson.
implementation 'com.google.code.gson:gson:2.8.5'
}
如果您想使用 coroutines
和 Retrofit
,请阅读俄语 https://medium.com/exploring-android/android-networking-with-coroutines-and-retrofit-a2f20dd40a83 (or https://habr.com/post/428994/。
您应该能够显着简化您的代码。声明您的用例类似于以下内容:
class UseCaseImpl {
suspend fun getCountry(countryId: Int): Country =
api.getCountry(countryId).awaitObject(CountryResponse.Deserializer()).country
suspend fun getRegion(regionId: Int): Region =
api.getRegion(regionId).awaitObject(RegionResponse.Deserializer()).region
suspend fun getCity(countryId: Int): City=
api.getCity(countryId).awaitObject(CityResponse.Deserializer()).city
}
现在您可以像这样编写 showLocation
函数:
private fun showLocation(
useCase: UseCaseImpl,
countryId: Int,
regionId: Int,
cityId: Int
) {
GlobalScope.launch(Dispatchers.Main) {
val countryTask = async { useCase.getCountry(countryId) }
val regionTask = async { useCase.getRegion(regionId) }
val cityTask = async { useCase.getCity(cityId) }
updateLocation(countryTask.await(), regionTask.await(), cityTask.await())
}
}
您无需在 IO
调度程序中启动,因为您的网络请求是非阻塞的。
我还必须注意,您不应在 GlobalScope
中启动。定义一个适当的协程范围,使其生命周期与 Android activity 或其父项的生命周期保持一致。
我想获得一个 API 请求并将请求的数据保存到数据库中。还要return的数据(即写入DB)。我知道,这在 RxJava 中是可能的,但现在我在 Kotlin 协程中编写,目前使用 Fuel 而不是 Retrofit(但差异不是那么大)。我看了
怎么写协程和方法?
更新
说,我们有一个 Java 和 Retrofit,RxJava。那我们就可以写一段代码了。
区域响应:
@AutoValue
public abstract class RegionResponse {
@SerializedName("id")
public abstract Integer id;
@SerializedName("name")
public abstract String name;
@SerializedName("countryId")
public abstract Integer countryId();
public static RegionResponse create(int id, String name, int countryId) {
....
}
...
}
地区:
data class Region(
val id: Int,
val name: String,
val countryId: Int)
网络:
public Single<List<RegionResponse>> getRegions() {
return api.getRegions();
// @GET("/regions")
// Single<List<RegionResponse>> getRegions();
}
区域存储库:
fun getRegion(countryId: Int): Single<Region> {
val dbSource = db.getRegion(countryId)
val lazyApiSource = Single.defer { api.regions }
.flattenAsFlowable { it }
.map { apiMapper.map(it) }
.toList()
.doOnSuccess { db.updateRegions(it) }
.flattenAsFlowable { it }
.filter({ it.countryId == countryId })
.singleOrError()
return dbSource
.map { dbMapper.map(it) }
.switchIfEmpty(lazyApiSource)
}
区域交互器:
class RegionInteractor(
private val repo: RegionRepository,
private val prefsRepository: PrefsRepository) {
fun getRegion(): Single<Region> {
return Single.fromCallable { prefsRepository.countryId }
.flatMap { repo.getRegion(it) }
.subscribeOn(Schedulers.io())
}
}
我们一层一层的来看
首先,据我所知,您的 RegionResponse
和 Region
对于这个用例来说完全没问题,所以我们根本不会碰它们。
您的网络层是用 Java 编写的,因此我们假设它始终需要同步行为,并且也不会触及它。
所以,我们从回购开始:
fun getRegion(countryId: Int) = async {
val regionFromDb = db.getRegion(countryId)
if (regionFromDb == null) {
return apiMapper.map(api.regions).
filter({ it.countryId == countryId }).
first().
also {
db.updateRegions(it)
}
}
return dbMapper.map(regionFromDb)
}
请记住,我没有您的代码,因此细节可能会有所不同。但是协程的一般想法是,如果他们需要 return 结果,你用 async()
启动它们,然后编写你的代码,就好像你在不需要的完美世界中一样关注并发。
现在是交互者:
class RegionInteractor(
private val repo: RegionRepository,
private val prefsRepository: PrefsRepository) {
fun getRegion() = withContext(Schedulers.io().asCoroutineDispatcher()) {
val countryId = prefsRepository.countryId
return repo.getRegion(countryId).await()
}
}
您需要一些东西来将异步代码转换回同步代码。为此,您需要某种线程池来执行。这里我们使用 Rx 的线程池,但如果你想使用其他池,也可以。
研究了awaitStringResponse
),又做了一个解决方案。假设您有 Kotlin 1.3 以及协程 1.0.0 和 Fuel 1.16.0。
我们必须避免带有回调的异步请求并进行同步(协程中的每个请求)。比如说,我们想通过代码显示国家名称。
// POST-request to a server with country id.
fun getCountry(countryId: Int): Request =
"map/country/"
.httpPost(listOf("country_id" to countryId))
.addJsonHeader()
// Adding headers to the request, if needed.
private fun Request.addJsonHeader(): Request =
header("Content-Type" to "application/json",
"Accept" to "application/json")
它给出了 JSON:
{
"country": {
"name": "France"
}
}
要解码 JSON 响应,我们必须编写一个模型 class:
data class CountryResponse(
val country: Country,
val errors: ErrorsResponse?
) {
data class Country(
val name: String
)
// If the server prints errors.
data class ErrorsResponse(val message: String?)
// Needed for awaitObjectResponse, awaitObject, etc.
class Deserializer : ResponseDeserializable<CountryResponse> {
override fun deserialize(content: String) =
Gson().fromJson(content, CountryResponse::class.java)
}
}
那么我们应该创建一个UseCase或者Interactor来同步接收一个结果:
suspend fun getCountry(countryId: Int): Result<CountryResponse, FuelError> =
api.getCountry(countryId).awaitObjectResponse(CountryResponse.Deserializer()).third
我使用 third
访问响应数据。但是,如果您希望检查 HTTP 错误代码 != 200,请删除 third
并稍后获取所有三个变量(作为 Triple
变量)。
现在你可以写一个打印国家名称的方法了。
private fun showLocation(
useCase: UseCaseImpl,
countryId: Int,
regionId: Int,
cityId: Int
) {
GlobalScope.launch(Dispatchers.IO) {
// Titles of country, region, city.
var country: String? = null
var region: String? = null
var city: String? = null
val countryTask = GlobalScope.async {
val result = useCase.getCountry(countryId)
// Receive a name of the country if it exists.
result.fold({ response -> country = response.country.name }
, { fuelError -> fuelError.message })
}
}
val regionTask = GlobalScope.async {
val result = useCase.getRegion(regionId)
result.fold({ response -> region = response.region?.name }
, { fuelError -> fuelError.message })
}
val cityTask = GlobalScope.async {
val result = useCase.getCity(cityId)
result.fold({ response -> city = response.city?.name }
, { fuelError -> fuelError.message })
}
// Wait for three requests to execute.
countryTask.await()
regionTask.await()
cityTask.await()
// Now update UI.
GlobalScope.launch(Dispatchers.Main) {
updateLocation(country, region, city)
}
}
}
在build.gradle
中:
ext {
fuelVersion = "1.16.0"
}
dependencies {
...
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
// Fuel.
//for JVM
implementation "com.github.kittinunf.fuel:fuel:${fuelVersion}"
//for Android
implementation "com.github.kittinunf.fuel:fuel-android:${fuelVersion}"
//for Gson support
implementation "com.github.kittinunf.fuel:fuel-gson:${fuelVersion}"
//for Coroutines
implementation "com.github.kittinunf.fuel:fuel-coroutines:${fuelVersion}"
// Gson.
implementation 'com.google.code.gson:gson:2.8.5'
}
如果您想使用 coroutines
和 Retrofit
,请阅读俄语 https://medium.com/exploring-android/android-networking-with-coroutines-and-retrofit-a2f20dd40a83 (or https://habr.com/post/428994/。
您应该能够显着简化您的代码。声明您的用例类似于以下内容:
class UseCaseImpl {
suspend fun getCountry(countryId: Int): Country =
api.getCountry(countryId).awaitObject(CountryResponse.Deserializer()).country
suspend fun getRegion(regionId: Int): Region =
api.getRegion(regionId).awaitObject(RegionResponse.Deserializer()).region
suspend fun getCity(countryId: Int): City=
api.getCity(countryId).awaitObject(CityResponse.Deserializer()).city
}
现在您可以像这样编写 showLocation
函数:
private fun showLocation(
useCase: UseCaseImpl,
countryId: Int,
regionId: Int,
cityId: Int
) {
GlobalScope.launch(Dispatchers.Main) {
val countryTask = async { useCase.getCountry(countryId) }
val regionTask = async { useCase.getRegion(regionId) }
val cityTask = async { useCase.getCity(cityId) }
updateLocation(countryTask.await(), regionTask.await(), cityTask.await())
}
}
您无需在 IO
调度程序中启动,因为您的网络请求是非阻塞的。
我还必须注意,您不应在 GlobalScope
中启动。定义一个适当的协程范围,使其生命周期与 Android activity 或其父项的生命周期保持一致。