How to use Fuel with coroutines in Kotlin?

2019-07-14 01:07发布

I want to get an API request and save request's data to a DB. Also want to return the data (that is written to DB). I know, this is possible in RxJava, but now I write in Kotlin coroutines, currently use Fuel instead of Retrofit (but a difference is not so large). I read How to use Fuel with a Kotlin coroutine, but don't understand it.

How to write a coroutine and methods?

UPDATE

Say, we have a Java and Retrofit, RxJava. Then we can write a code.

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

Region:

data class Region(
    val id: Int,
    val name: String,
    val countryId: Int)

Network:

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条回答
ゆ 、 Hurt°
2楼-- · 2019-07-14 01:19

You should be able to significantly simplify your code. Declare your use case similar to the following:

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
}

Now you can write your showLocation function like this:

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

You have no need to launch in the IO dispatcher because your network requests are non-blocking.

I must also note that you shouldn't launch in the GlobalScope. Define a proper coroutine scope that aligns its lifetime with the lifetime of the Android activity or whatever else its parent is.

查看更多
迷人小祖宗
3楼-- · 2019-07-14 01:23

After researching How to use Fuel with a Kotlin coroutine, Fuel coroutines and https://github.com/kittinunf/Fuel/ (looked for awaitStringResponse), I made another solution. Assume that you have Kotlin 1.3 with coroutines 1.0.0 and Fuel 1.16.0.

We have to avoid asynhronous requests with callbacks and make synchronous (every request in it's coroutine). Say, we want to show a country name by it's code.

// 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")

It gives a JSON:

{
  "country": {
    "name": "France"
  }
}

To decode the JSON response we have to write a model 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)
    }
}

Then we should create a UseCase or Interactor to receive a result synchronously:

suspend fun getCountry(countryId: Int): Result<CountryResponse, FuelError> =
    api.getCountry(countryId).awaitObjectResponse(CountryResponse.Deserializer()).third

I use third to access response data. But if you wish to check for a HTTP error code != 200, remove third and later get all three variables (as Triple variable).

Now you can write a method to print the country name.

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

In 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'
}

If you want to work with coroutines and Retrofit, please, read https://medium.com/exploring-android/android-networking-with-coroutines-and-retrofit-a2f20dd40a83 (or https://habr.com/post/428994/ in Russian).

查看更多
Rolldiameter
4楼-- · 2019-07-14 01:42

Let's look at it layer by layer.

First, your RegionResponse and Region are totally fine for this use case, as far as I can see, so we won't touch them at all.

Your network layer is written in Java, so we'll assume it always expects synchronous behavior, and won't touch it either.

So, we start with the repo:

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

Remember that I don't have your code, so maybe the details will differ a bit. But the general idea with coroutines, is that you launch them with async() in case they need to return the result, and then write your code as if you were in the perfect world where you don't need to concern yourself with concurrency.

Now to the interactor:

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

You need something to convert from asynchronous code back to synchronous one. And for that you need some kind of thread pool to execute on. Here we use thread pool from Rx, but if you want to use some other pool, so do.

查看更多
登录 后发表回答