使用 Corrutines 的 Kotlin retrofit2 连接

Posted

技术标签:

【中文标题】使用 Corrutines 的 Kotlin retrofit2 连接【英文标题】:Kotlin retrofit2 connection using corrutines 【发布时间】:2021-12-23 02:21:08 【问题描述】:

我正在 Kotlin 中开发一个应用程序,它使用 Retrofit2 库与 PokeApi 连接。

我曾尝试使用 corrutines 来做到这一点,但我开始获得的响应为 null,而在使用 corrutines 之前,通过异步调用。

调用 API 的 dataProvider 的代码如下:

DataProvider.kt

   /**
     * Metodo que permite obtener un LiveData con la informacion del siguiente pokemon por id, que será la posible evolución.
     */
    fun viewPokemonEvolution(id: Long): LiveData<PokemonFormResponse>? 

        var remotePokemonEvolutionFormData : LiveData<PokemonFormResponse>? =  null
        var call: Response<LiveData<PokemonFormResponse>>
        var data:  LiveData<PokemonFormResponse>?
        CoroutineScope(Dispatchers.Main).launch 
            call = remoteDataSource.downloadPokemonViewedData(id)
            data = call.body()
            if(call.isSuccessful)
                remotePokemonEvolutionFormData = data
            
        
        return remotePokemonEvolutionFormData!!
    

我的 API 类

interface PokemonApi 

    @GET("pokemon-form/id")
    suspend fun getPokemonInfo(@Path("id") idPokemon: Long): Response<LiveData<PokemonFormResponse>>

    @GET("pokemon/name")
    suspend fun getPokemonExtendedInfo(@Path("name") pokemonName: String): Response<LiveData<PokemonExtendedInfoResponse>>


我的数据类

PokemoFormResponse.kt

data class PokemonFormResponse(
    @SerializedName("form_name")
    val formName: String,
    @SerializedName("form_names")
    val formNames: List<Any>,
    @SerializedName("form_order")
    val formOrder: Int,
    @SerializedName("id")
    val id: Int,
    @SerializedName("is_battle_only")
    val isBattleOnly: Boolean,
    @SerializedName("is_default")
    val isDefault: Boolean,
    @SerializedName("is_mega")
    val isMega: Boolean,
    @SerializedName("name")
    val name: String,
    @SerializedName("names")
    val names: List<Any>,
    @SerializedName("order")
    val order: Int,
    @SerializedName("pokemon")
    val pokemon: PokemonUrl,
    @SerializedName("sprites")
    val sprites: Sprites,
    @SerializedName("version_group")
    val versionGroup: VersionGroup
)
    fun idFilledWithZero(): String 
        return String.format("%03d", id)
    

我的远程数据源

IRemoteDataSource.kt

interface IRemoteDataSource 

    suspend fun downloadPokemonViewedData(id: Long): Response<LiveData<PokemonFormResponse>>
    suspend fun downloadPokemonCatchedData(name: String): Response<LiveData<PokemonExtendedInfoResponse>>


interface ILocalDataSource 

    fun getPokemonList(): LiveData<List<Pokemon>>
    fun getPokemonById(idPokemon: Long): LiveData<Pokemon>
   suspend fun insertPokemon(pokemon: Pokemon)

以及在LocalDataSource中调用房间的DAO:

@Dao
interface PokemonDao 

    @Query("SELECT * from listaPokemon")
    fun getAll(): LiveData<List<Pokemon>>

    @Insert(onConflict = REPLACE)
    fun insert(pokemon:Pokemon)

    @Insert(onConflict = REPLACE)
    fun insertAll(vararg pokemons: Pokemon)

    @Query("SELECT * from listaPokemon WHERE id = :pokemonId")
    fun getById(pokemonId: Long): LiveData<Pokemon>


我希望你能提出一些修复实现的方法,因为这样更好的方法是使用 corrutines 而不是异步调用。

我添加了一个debugg的捕获显示所有属性为null

希望您能帮上忙,如果是这样,请提前致谢!

[编辑]

添加远程数据源`


class RemoteDataSource : IRemoteDataSource

    val BASE_URL = "https://pokeapi.co/api/v2/"
    val TIMEOUT: Long = 30

    var apiServices: PokemonApi

    init 
        val httpClient : OkHttpClient.Builder = OkHttpClient.Builder()
        httpClient.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
        httpClient.readTimeout(TIMEOUT, TimeUnit.SECONDS)
        httpClient.writeTimeout(TIMEOUT, TimeUnit.SECONDS)

        val retrofit: Retrofit = Retrofit.Builder()
       //Se utiliza el networkIO como ejecutor de Retrofit
            .callbackExecutor(AppExecutors.networkIO)
            .client(httpClient.build())
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .build()

        apiServices = retrofit.create(PokemonApi::class.java)
    


    /**
     * Función tipo utilizando retrofit para obtener datos desde una api remota
     *
     * Simplicación de las funciones mediante las corrutinas
     */
    override suspend fun downloadPokemonViewedData(id: Long): Response<LiveData<PokemonFormResponse>> = withContext(Dispatchers.Main) 
        apiServices.getPokemonInfo(
            id
        )
    

    override suspend fun downloadPokemonCatchedData(name: String): Response<LiveData<PokemonExtendedInfoResponse>> = withContext(Dispatchers.Main)
        apiServices.getPokemonExtendedInfo(
            name
        )
    

还有界面

IRemoteDatasource.kt

interface IRemoteDataSource 

    suspend fun downloadPokemonViewedData(id: Long): Response<LiveData<PokemonFormResponse>>
    suspend fun downloadPokemonCatchedData(name: String): Response<LiveData<PokemonExtendedInfoResponse>>

【问题讨论】:

你在哪里有 GET 请求? IRemoteDataSource 是做什么的? 对不起,我已经加了,IRemoteDataSource是RemoteDataSource的接口,只是stablish方法,必须实现 suspend fun getPokemonInfo(@Path("id") idPokemon: Long): Response&lt;LiveData&lt;PokemonFormResponse&gt;&gt; 为什么要返回包裹在 LiveData 中的原始回复,然后又返回包裹在 Response 中的回复。为什么不直接返回PokemonFormResponse 主要目标是我需要使用 LiveData 来管理视图模型,而响应包装是因为我将其视为封装调用 API 的最佳方式 您可以从 api 返回 PokemonFormResponse 并将其转换为 ViewModel 或 Data Provider 中的实时数据(或流)。 This 文章可能会有所帮助。我认为您无法从这样的改造中获得实时数据(我猜至少没有额外的转换器工厂) 【参考方案1】:

在尝试实施@Arpit Shukla 解决方案后,我遇到了调用 viewModelDetail loadPokemonInfo(id) 和 loadPokemonEvolution(id) 的结果的问题,因为它们都返回 LiveData(Unit) 类型,而不仅仅是单位与更改前一样。

所以我像这样在 DetailFragment.kt 上调用这些方法的结果,但不起作用

PokemonDetailFragment.kt

....

fun observeViewModel()
        Log.d("info", "Entra en observeViewModel")

        pokemonListViewModel?.selectedPokemonIdFromVM?.observe(viewLifecycleOwner, Observer  selectedId ->
            selectedId?.let  pokemonSelectedId ->
                pokemonDetailViewModel?.loadPokemonInfo(pokemonSelectedId)
                //Al seleccionar el pokemeon actualizamos tambien el evlution del ViewModelDetail
                pokemonDetailViewModel?.loadPokemonEvolution(pokemonSelectedId)
            
        )
            ?:
            pokemonIdFromBundle?.let 
                pokemonDetailViewModel?.loadPokemonInfo(it)
                pokemonDetailViewModel?.loadPokemonEvolution(it)
            
.....

所以我尝试观察响应,所以它是 LiveData,但我不知道我应该对回调做什么。

我的其他班级如下

PokemonApi.kt

interface PokemonApi 

    @GET("pokemon-form/id")
    suspend fun getPokemonInfo(@Path("id") idPokemon: Long): PokemonFormResponse

    @GET("pokemon/name")
    suspend fun getPokemonExtendedInfo(@Path("name") pokemonName: String): PokemonExtendedInfoResponse


IRemoteDataSource.kt

interface IRemoteDataSource 

    suspend fun downloadPokemonViewedData(id: Long): PokemonFormResponse
    suspend fun downloadPokemonCatchedData(name: String): PokemonExtendedInfoResponse

RemoteDataSource.kt


class RemoteDataSource : IRemoteDataSource

    val BASE_URL = "https://pokeapi.co/api/v2/"
    val TIMEOUT: Long = 30

    var apiServices: PokemonApi

    init 
        val httpClient : OkHttpClient.Builder = OkHttpClient.Builder()
        httpClient.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
        httpClient.readTimeout(TIMEOUT, TimeUnit.SECONDS)
        httpClient.writeTimeout(TIMEOUT, TimeUnit.SECONDS)

        val retrofit: Retrofit = Retrofit.Builder()
       //Se utiliza el networkIO como ejecutor de Retrofit
            .callbackExecutor(AppExecutors.networkIO)
            .client(httpClient.build())
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .build()

        apiServices = retrofit.create(PokemonApi::class.java)
    
    /**
     * Función tipo utilizando retrofit para obtener datos desde una api remota
     *
     * Simplicación de las funciones mediante las corrutinas
     */
    override suspend fun downloadPokemonViewedData(id: Long): PokemonFormResponse = withContext(Dispatchers.Main) 
        apiServices.getPokemonInfo(
            id
        )
    

    override suspend fun downloadPokemonCatchedData(name: String): PokemonExtendedInfoResponse = withContext(Dispatchers.Default)
        apiServices.getPokemonExtendedInfo(
            name
        )
    

关于本地数据库。

口袋妖怪道

@Dao
interface PokemonDao 

    @Query("SELECT * from listaPokemon")
    fun getAll(): List<Pokemon>

    @Insert(onConflict = REPLACE)
    fun insert(pokemon:Pokemon)

    @Insert(onConflict = REPLACE)
    fun insertAll(vararg pokemons: Pokemon)

    @Query("SELECT * from listaPokemon WHERE id = :pokemonId")
    fun getById(pokemonId: Long): Pokemon


AppDatabase.kt


//Definición de la DB ROOM y sus entities
@Database(entities = arrayOf(Pokemon::class), version = 1)
abstract class AppDatabase : RoomDatabase() 


    //Singleton de la DB
    companion object 
        private var instance: AppDatabase? = null

        fun getInstance(context: Context):AppDatabase? 
            if (instance == null)
                synchronized(AppDatabase::class)
                    //datos del objeto sql
                    instance = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "pokedexcanner.db")
                        .fallbackToDestructiveMigration().build()
                
            
            return instance
        
    

    //obtención de los DAOs de la DB
    abstract fun getPokemonDao(): PokemonDao

ILocalDataSource.kt

interface ILocalDataSource 

    fun getPokemonList(): List<Pokemon>
    fun getPokemonById(idPokemon: Long): Pokemon
   suspend fun insertPokemon(pokemon: Pokemon)

LocalDataSource.kt

class LocalDataSource : ILocalDataSource

    lateinit var pokemonDao: PokemonDao

    constructor(context: Context)
        val database = AppDatabase.getInstance(context)
        database?.let 
            pokemonDao = database.getPokemonDao()
        
    

    override fun getPokemonList(): List<Pokemon> 
            return pokemonDao.getAll()
    

    override fun getPokemonById(idPokemon: Long): Pokemon
        return pokemonDao.getById(idPokemon)

    

    override suspend fun insertPokemon(pokemon: Pokemon) 
        pokemonDao.insert(pokemon)
    


实体方面:

口袋妖怪.kt

@Entity(tableName = "listaPokemon")
data class Pokemon (@PrimaryKey var id: Long?,
                    @ColumnInfo(name = "name") var nombre: String,
                    @ColumnInfo(name = "image") var imagen: String?,
                    @ColumnInfo(name = "height") var altura: Float?,
                    @ColumnInfo(name = "weight") var peso: Float?
)
    fun idFilledWithZero(): String 
       return String.format("%03d", id)
    

    constructor():this(null,"?",null,null,null)

` PokemonFormResponse.kt


data class PokemonFormResponse(
    @SerializedName("form_name")
    val formName: String,
    @SerializedName("form_names")
    val formNames: List<Any>,
    @SerializedName("form_order")
    val formOrder: Int,
    @SerializedName("id")
    val id: Int,
    @SerializedName("is_battle_only")
    val isBattleOnly: Boolean,
    @SerializedName("is_default")
    val isDefault: Boolean,
    @SerializedName("is_mega")
    val isMega: Boolean,
    @SerializedName("name")
    val name: String,
    @SerializedName("names")
    val names: List<Any>,
    @SerializedName("order")
    val order: Int,
    @SerializedName("pokemon")
    val pokemon: PokemonUrl,
    @SerializedName("sprites")
    val sprites: Sprites,
    @SerializedName("version_group")
    val versionGroup: VersionGroup
)
    fun idFilledWithZero(): String 
        return String.format("%03d", id)
    

PokemonExtendedInfoResponse.kt

data class PokemonExtendedInfoResponse(
    @SerializedName("abilities")
    val abilities: List<Ability>,
    @SerializedName("base_experience")
    val baseExperience: Int,
    @SerializedName("forms")
    val forms: List<Form>,
    @SerializedName("game_indices")
    val gameIndices: List<GameIndice>,
    @SerializedName("height")
    val height: Float,
    @SerializedName("held_items")
    val heldItems: List<HeldItem>,
    @SerializedName("id")
    val id: Int,
    @SerializedName("is_default")
    val isDefault: Boolean,
    @SerializedName("location_area_encounters")
    val locationAreaEncounters: String,
    @SerializedName("moves")
    val moves: List<Move>,
    @SerializedName("name")
    val name: String,
    @SerializedName("order")
    val order: Int,
    @SerializedName("species")
    val species: Species,
    @SerializedName("sprites")
    val sprites: SpritesX,
    @SerializedName("stats")
    val stats: List<Stat>,
    @SerializedName("types")
    val types: List<Type>,
    @SerializedName("weight")
    val weight: Float
)

因此,如果您知道一些处理这种情况的方法,请提前感谢!

【讨论】:

以上是关于使用 Corrutines 的 Kotlin retrofit2 连接的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Kotlin 中将函数接收器类型与 SAM 接口一起使用

【Android Kotlin】关于使用Http来获取数据的基本套路

Kotlin - 通过 Intent 将函数作为参数传递

如何使用 JPA 和休眠映射 Java/Kotlin 字符串数组和 Postgres SQL 数组

android 11 kotlin 中的包可见性

添加 kotlin 协程和房间后无法进行发布构建,proguard 警告