Как использовать Топливо с сопрограммами в Котлине? - PullRequest
0 голосов
/ 03 сентября 2018

Я хочу получить запрос API и сохранить данные запроса в БД. Также хочу вернуть данные (которые пишутся в БД). Я знаю, это возможно в RxJava, но сейчас я пишу в сопрограммах Kotlin, в настоящее время использую Fuel вместо Retrofit (но разница не так велика). Я читаю Как использовать Fuel с сопрограммой Kotlin , но не понимаю этого.

Как написать сопрограмму и методы?

UPDATE

Скажем, у нас есть Java и 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 голосов
/ 09 ноября 2018

После исследования Как использовать Fuel с сопрограммой Kotlin , сопрограммы Fuel и 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, нам нужно написать класс модели:

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 (или https://habr.com/post/428994/ на русском языке).

0 голосов
/ 09 ноября 2018

Вы должны быть в состоянии значительно упростить свой код. Объявите ваш вариант использования, подобный следующему:

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 или кем бы то ни было его родитель.

0 голосов
/ 11 сентября 2018

Давайте посмотрим на это слой за слоем.

Во-первых, ваши 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)
}

Помните, что у меня нет вашего кода, поэтому, возможно, детали будут немного отличаться. Но общая идея сопрограмм состоит в том, что вы запускаете их с 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, но если вы хотите использовать какой-то другой пул, так и сделайте.

...