I was developing an App in Kotlin which connect using Retrofit2 library with the PokeApi.
I'd ve tried to do it using corrutines but the Response which i've started to get are null, while before using corrutines, via asynchronous call.
The Code of my dataProvider, which make the call to the API is the following:
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!!
}
My API class
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>>
}
My data class
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)
}
}
My remoteDatasource
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)
}
And the DAO which call room in the LocalDataSource:
@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>
}
I hope yu can suggest some way to fix the implementation, due to is such better way use corrutines instead of asynchronous calls.
I add a capture of the debugg show all the atributes null
I hope you can help, and if it's like this tke thanks in advance !
[EDIT]
ADDED RemoteDataSource`
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
)
}
And the interface
IRemoteDatasource.kt
interface IRemoteDataSource {
suspend fun downloadPokemonViewedData(id: Long): Response<LiveData<PokemonFormResponse>>
suspend fun downloadPokemonCatchedData(name: String): Response<LiveData<PokemonExtendedInfoResponse>>
}
After try to implement the @Arpit Shukla Solution, I'm in the problem of call the result of my viewModelDetail loadPokemonInfo(id) and loadPokemonEvolution(id), due to both return an LiveData(Unit) type, instead of just unit as before the changes.
So I invoque the result of those method on the DetailFragment.kt like this, and doesn't work
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)
}
.....
So i try to observe the response, so it a LiveData, but I don't know what should i do on the callback.
My others class are the following
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
)
}
}
Regarding localDatabase.
PokemonDao
@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)
}
}
And Entities side:
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
)
So if you know some way of manage this situation take thanks in advance !