如何在 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())
    }
}

我们一层一层的来看

首先,据我所知,您的 RegionResponseRegion 对于这个用例来说完全没问题,所以我们根本不会碰它们。

您的网络层是用 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'
}

如果您想使用 coroutinesRetrofit,请阅读俄语 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 或其父项的生命周期保持一致。