如何在Kotlin中使用带有协同程序的Fuel?

时间:2018-09-03 08:10:03

标签: android kotlin kotlin-coroutines

我想获取一个API请求并将请求的数据保存到DB。还想返回数据(即写入数据库)。我知道,这在RxJava中是可能的,但是现在我在Kotlin协程中编写,当前使用Fuel而不是Retrofit(但相差并不大)。我读过How to use Fuel with a Kotlin coroutine,但听不懂。

如何编写协程和方法?

更新

说,我们有一个Java and Retrofit,RxJava。然后我们可以编写代码。

RegionResponse:

@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();
}

RegionRepository:

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

RegionInteractor:

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())
    }
}

3 个答案:

答案 0 :(得分:1)

让我们逐层查看它。

首先,据我所知,您的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)
}

请记住,我没有您的代码,所以细节可能有所不同。但是协程的一般想法是,用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中的线程池,但是如果您想使用其他池,也可以。

答案 1 :(得分:1)

研究了How to use Fuel with a Kotlin coroutineFuel coroutineshttps://github.com/kittinunf/Fuel/(寻找awaitStringResponse)之后,我提出了另一个解决方案。假设您拥有带有协同程序1.0.0和Fuel 1.16.0的Kotlin 1.3。

我们必须避免异步请求和回调,并使其同步(每个请求都在协程中)。说,我们要通过代码显示国家/地区名称。

// 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响应,我们必须编写一个模型类:

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(或俄语的https://habr.com/post/428994/)。

答案 2 :(得分:1)

您应该能够大大简化您的代码。声明类似于以下内容的用例:

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活动或其他活动的生命周期保持一致。