Android Kotlin Retrofit2 doesn't return data from web server: "unable to create converter for ApiService get method"

58 Views Asked by At

My app crashes when opening with this message:

java.lang.IllegalArgumentException: Unable to create converter for java.util.List<com.dov4k1n.vkinternshiptask.network.Products>
    for method ProductsApiService.getProductsList
 at retrofit2.Utils.methodError(Utils.java:54)
 at retrofit2.HttpServiceMethod.createResponseConverter(HttpServiceMethod.java:126)
 at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:85)
 at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:39)
 at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:202)
 at retrofit2.Retrofit$1.invoke(Retrofit.java:160)
 at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
 at $Proxy2.getProductsList(Unknown Source)
 at ...

I want to retrieve products' info from web server https://dummyjson.com/products with Retrofit2 library using Kotlin and Android Studio.

This is my json:

{
    "products": [
        {
            "id": 1,
            "title": "iPhone 9",
            "description": "An apple mobile which is nothing like apple",
            "price": 549,
            "discountPercentage": 12.96,
            "rating": 4.69,
            "stock": 94,
            "brand": "Apple",
            "category": "smartphones",
            "thumbnail": "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg",
            "images": [
                "https://cdn.dummyjson.com/product-images/1/1.jpg",
                "https://cdn.dummyjson.com/product-images/1/2.jpg",
                "https://cdn.dummyjson.com/product-images/1/3.jpg",
                "https://cdn.dummyjson.com/product-images/1/4.jpg",
                "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg"
            ]
        },
        {
            "id": 2,
            "title": "iPhone X",
            "description": "SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...",
            "price": 899,
            "discountPercentage": 17.94,
            "rating": 4.44,
            "stock": 34,
            "brand": "Apple",
            "category": "smartphones",
            "thumbnail": "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg",
            "images": [
                "https://cdn.dummyjson.com/product-images/2/1.jpg",
                "https://cdn.dummyjson.com/product-images/2/2.jpg",
                "https://cdn.dummyjson.com/product-images/2/3.jpg",
                "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg"
            ]
        },
        {
            "id": 3,
            "title": "Samsung Universe 9",
            "description": "Samsung's new variant which goes beyond Galaxy to the Universe",
            "price": 1249,
            "discountPercentage": 15.46,
            "rating": 4.09,
            "stock": 36,
            "brand": "Samsung",
            "category": "smartphones",
            "thumbnail": "https://cdn.dummyjson.com/product-images/3/thumbnail.jpg",
            "images": [
                "https://cdn.dummyjson.com/product-images/3/1.jpg"
            ]
        }
    ],
    "total": 100,
    "skip": 0,
    "limit": 30
}

This is my Products data class:

data class Products(
    val id: Int,
    val title: String,
    val description: String,
    val price: Double,
    val discountPercentage: Double,
    val rating: Double,
    val stock: Int,
    val brand: String,
    val category: String,
    val thumbnail: String,
    val images: List<String>
)

This is my ProductsApiService:

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET

private const val BASE_URL = "https://dummyjson.com"
private val retrofit = Retrofit.Builder()
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
    .baseUrl(BASE_URL)
    .build()

interface ProductsApiService {
    @GET("/products")
    suspend fun getProductsList(): List<Products>
}

object ProductsApi {
    val retrofitService : ProductsApiService by lazy {
        retrofit.create(ProductsApiService::class.java)
    }
}

It seems like getProductsList() function doesn't work as expected

This is my ProductsViewModel where I use this api service, if it helps:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dov4k1n.vkinternshiptask.network.ProductsApi
import kotlinx.coroutines.launch
import java.io.IOException

sealed interface ProductsUiState {
    data class Success(val products: String) : ProductsUiState
    object Error: ProductsUiState
    object Loading: ProductsUiState
}

class ProductsViewModel : ViewModel() {
    var productsUiState: ProductsUiState by mutableStateOf(ProductsUiState.Loading)
        private set

    init {
        getProducts()
    }

    fun getProducts() {
        viewModelScope.launch {
            productsUiState = try {
                val listResult = ProductsApi.retrofitService.getProductsList()
                ProductsUiState.Success(
                    "Success: ${listResult.size} products retrieved"
                )
            }
            catch (e: IOException) {
                ProductsUiState.Error
            }
        }
    }
}

I use this in dependencies:

// Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    // Retrofit with Kotlin serialization Converter
    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
    implementation("com.squareup.okhttp3:okhttp:4.11.0")
    // Kotlin serialization
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

and this plugin:

id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22"

I did almost everything as in the Google's codelab https://developer.android.com/codelabs/basic-android-kotlin-compose-getting-data-internet

But in the codelab json https://android-kotlin-fun-mars-server.appspot.com/photos is like this:

[
  {
    "id": "424905",
    "img_src": "https://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631300503690E01_DXXX.jpg"
  },
  {
    "id": "424906",
    "img_src": "https://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
  },
  {
    "id": "424907",
    "img_src": "https://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631290503689E01_DXXX.jpg"
  },
  {
    "id": "424908",
    "img_src": "https://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631290305226E03_DXXX.jpg"
  }
]

MarshPhoto data class:

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MarsPhoto(
    val id: String,
    @SerialName(value = "img_src")
    val imgSrc: String
)

MarsApiService:

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET

private const val BASE_URL =
    "https://android-kotlin-fun-mars-server.appspot.com"
private val retrofit = Retrofit.Builder()
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
    .baseUrl(BASE_URL)
    .build()

interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}

object MarsApi {
    val retrofitService : MarsApiService by lazy {
        retrofit.create(MarsApiService::class.java)
    }
}

MarsViewModel:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.marsphotos.network.MarsApi
import kotlinx.coroutines.launch
import java.io.IOException

sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error: MarsUiState
    object Loading: MarsUiState
}

class MarsViewModel : ViewModel() {
    /** The mutable State that stores the status of the most recent request */
    var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
        private set

    /**
     * Call getMarsPhotos() on init so we can display status immediately.
     */
    init {
        getMarsPhotos()
    }

    /**
     * Gets Mars photos information from the Mars API Retrofit service and updates the
     * [MarsPhoto] [List] [MutableList].
     */
    fun getMarsPhotos() {
        viewModelScope.launch {
            marsUiState = try {
                val listResult = MarsApi.retrofitService.getPhotos()
                MarsUiState.Success(
                    "Success: ${listResult.size} Mars photos retrieved"
                )
            }
            catch (e: IOException) {
                MarsUiState.Error
            }
        }
    }
}

Their code doesn't crash and work as expected. Can't understand why my code does crash

2

There are 2 best solutions below

0
Ayzat Rizatdinov On BEST ANSWER

I didn't understand well json format, figured it out only after I posted this question.

This is an element, which contains List called "products", Int called "total", Int called "skip" and Int called "limit":

{
    "products": [
        {
            "id": 1,
            "title": "iPhone 9",
            "description": "An apple mobile which is nothing like apple",
            "price": 549,
            "discountPercentage": 12.96,
            "rating": 4.69,
            "stock": 94,
            "brand": "Apple",
            "category": "smartphones",
            "thumbnail": "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg",
            "images": [
                "https://cdn.dummyjson.com/product-images/1/1.jpg",
                "https://cdn.dummyjson.com/product-images/1/2.jpg",
                "https://cdn.dummyjson.com/product-images/1/3.jpg",
                "https://cdn.dummyjson.com/product-images/1/4.jpg",
                "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg"
            ]
        },
        {
            "id": 2,
            "title": "iPhone X",
            "description": "SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...",
            "price": 899,
            "discountPercentage": 17.94,
            "rating": 4.44,
            "stock": 34,
            "brand": "Apple",
            "category": "smartphones",
            "thumbnail": "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg",
            "images": [
                "https://cdn.dummyjson.com/product-images/2/1.jpg",
                "https://cdn.dummyjson.com/product-images/2/2.jpg",
                "https://cdn.dummyjson.com/product-images/2/3.jpg",
                "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg"
            ]
        }
    ],
    "total": 100,
    "skip": 0,
    "limit": 30
}

So in my ProductsApiService I should get proper response:

private const val BASE_URL = "https://dummyjson.com"
private val retrofit = Retrofit.Builder()
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
    .baseUrl(BASE_URL)
    .build()

interface ProductsApiService {
    @GET("products")
    suspend fun getProductsResponse(): ProductsResponse
}

P.S. also I removed "/" in @GET("/products"). Maybe that was significant too.

ProductsResponse:

import kotlinx.serialization.Serializable

@Serializable
data class ProductsResponse(
    val products: List<Products>,
    val total: Int,
    val skip: Int,
    val limit: Int
)

Products:

import kotlinx.serialization.Serializable

@Serializable
data class Products(
    val id: Int,
    val title: String,
    val description: String,
    val price: Double,
    val discountPercentage: Double,
    val rating: Double,
    val stock: Int,
    val brand: String,
    val category: String,
    val thumbnail: String,
    val images: List<String>
)

Now it works as I expected.

2
Binay Shaw On

Aahh, fyi, price: Long is acceptable in your case. You can also use Int anyway.