Search code examples
androidkotlinandroid-jetpack-composespoonacular

Jetpack Compose - Receiving response from API call, but cannot store the data


I'm building an Android recipe app, using the Spoonacular API. (I'm quite new to jetpack compose)

The issue is, the value stored inside of _randomRecipeResult in the viewModel is supposed to be the Json object I believe, or well, it's definitely not supposed to be null, and I'm not sure where I'm going wrong.

I am calling their getRandomRecipes endpoint. The logcat is showing this as the regular okhttp response and below I'm trying to log the response value: LogCat-New picture

I will provide the code for my retrofit builder, endpoints, model, viewmodel and repository. I am then calling the getRandomResults from one of my screens

RetroFit:

class Api {
    companion object {
        const val RECIPE_BASE_URL = "https://api.spoonacular.com/recipes/"
        const val apiKey = "secret"
        //const val CHAT_BASE_URL = "" not needed actually

        val recipeClient by lazy { createApi(RECIPE_BASE_URL, apiKey) }

        private fun createApi(baseUrl: String, apiKey: String): ApiService {
            val client = OkHttpClient.Builder().apply {
                addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
                readTimeout(10, TimeUnit.SECONDS)
                writeTimeout(10, TimeUnit.SECONDS)
            }.build()

            return Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiService::class.java)

        }
    }
}

EndPoints:

interface ApiService {

    // get random recipes for discoverScreen
    @GET("random?")
    suspend fun getRandomRecipe(
        @Query("apiKey") apiKey: String,
        @Query("number") number: Int // how many random recipes to return
    ): RandomResults

    // Searching for recipes is done in 2 steps
    // First:
    @GET("complexSearch")
    suspend fun searchRecipe(
        @Query("apiKey") apiKey: String,
        @Query("query") query: String,
        @Query("number") number: Int,
        // there are more things parameters but I'm not fully interested in them
    ): Recipe

    // Second:
    @GET("{id}/information?") //{id} needs to be passed, received from "searchRecipe"
    suspend fun getRecipeInfo(
        @Query("apiKey") apiKey: String,
        @Query("id") id: Int
    ): Recipe

}

Model:

data class Recipe(
    @SerializedName("id") val id: Int,
    @SerializedName("title") val title: String,
    @SerializedName("image") val image: String,
    @SerializedName("servings") val servings: BigDecimal, // might be type: BigDecimal
    @SerializedName("readyInMinutes") val readyInMinutes: Int,
    @SerializedName("instructions") val instructions: List<String>,
    @SerializedName("analyzedInstructions") val analyzedInstructions: List<AnalyzedInstructions>,
    @SerializedName("summary") val summary: String //not sure if it will be used
)

data class RandomResults(
    @SerializedName("recipes") val results: List<Recipe>
)

data class AnalyzedInstructions(
    val name: String = "",
    val steps: List<Step>
)

data class Step(
    val equipment: List<Equipment>,
    val ingredients: List<Ingredient>,
    val length: Length,
    val number: Int = 0,
    val step: String = ""
)

data class Equipment(
    val id: Int = 0,
    val image: String = "",
    val localizedName: String = "",
    val name: String = "",
    val temperature: Temperature
)

data class Ingredient(
    val id: Int = 0,
    val image: String = "",
    val localizedName: String = "",
    val name: String = ""
)

data class Length(
    val number: Int = 0,
    val unit: String = ""
)

data class Temperature(
    val number: Double = 0.0,
    val unit: String = ""
)

Repository:

class RecipeRepository(private val client: OkHttpClient) {

    private val recipeApiService: ApiService = Api.recipeClient

    init {
        val retrofit = Retrofit.Builder()
            .baseUrl(Api.RECIPE_BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    suspend fun getRandomRecipes(apiKey: String, number: Int): Resource<RandomResults> {
        val response = try {
            withTimeout(5_000) {
                recipeApiService.getRandomRecipe(apiKey, number)
            }
        } catch(e: Exception) {
            Log.e("RecipeRepository", e.message ?: "ERROR")
            return Resource.Error("ERROR!!")
        }
        return Resource.Success(response)
    }

    suspend fun searchRecipe(apiKey: String, query: String, number: Int): Resource<Recipe> {
        val response = try {
            withTimeout(5_000) {
                recipeApiService.searchRecipe(apiKey, query, number)
            }
        } catch(e: Exception) {
            Log.e("RecipeRepository", e.message ?: "ERROR")
            return Resource.Error("ERROR!!")
        }
        Log.d("API RESPONSE", response.toString())
        println(response.toString())
        return Resource.Success(response)
    }

    suspend fun getRecipeInfo(apiKey: String, id: Int): Resource<Recipe> {
        val response = try {
            withTimeout(5_000) {
                recipeApiService.getRecipeInfo(apiKey, id)
            }
        } catch(e: Exception) {
            Log.e("RecipeRepository", e.message ?: "ERROR")
            return Resource.Error("ERROR!!")
        }
        return Resource.Success(response)
    }
}

ViewModel:

class RecipeViewModel(application: Application) : AndroidViewModel(application) {

    private val recipeRepository: RecipeRepository

    private val client: OkHttpClient

    init {
        val loggingInterceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

        client = OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build()

        recipeRepository = RecipeRepository(client)
    }
    // getRandomRecipes
    val randomRecipeResults: MutableLiveData<Resource<RandomResults>>
        get() = _randomRecipeResults
    private val _randomRecipeResults: MutableLiveData<Resource<RandomResults>> = MutableLiveData(Resource.Empty())

    // searchRecipe
    val searchRecipeResults: MutableLiveData<Resource<Recipe>>
        get() = _searchRecipeResults
    private val _searchRecipeResults: MutableLiveData<Resource<Recipe>> = MutableLiveData(Resource.Empty())

    // getRecipeInfo
    val recipeInfoResults: MutableLiveData<Resource<Recipe>>
        get() = _recipeInfoResults
    private val _recipeInfoResults: MutableLiveData<Resource<Recipe>> = MutableLiveData(Resource.Empty())


    // Functions
    fun getRandomRecipes(apiKey: String, number: Int) {
        _randomRecipeResults.value = Resource.Loading()

        viewModelScope.launch {
            _randomRecipeResults.value = recipeRepository.getRandomRecipes(apiKey, number)
            Log.d("results:", _randomRecipeResults.value!!.data.toString())
        }
    }

    fun searchRecipe(apiKey: String, query: String, number: Int) {
        _searchRecipeResults.value = Resource.Loading()

        viewModelScope.launch {

            _searchRecipeResults.value = recipeRepository.searchRecipe(apiKey, query, number)
        }
    }

    fun getRecipeInfo(apiKey:String, id: Int) {
        _recipeInfoResults.value = Resource.Loading()
        viewModelScope.launch {
            _recipeInfoResults.value = recipeRepository.getRecipeInfo(apiKey, id)
        }
    }
}

Solution

  • Please update your RandomResults class. It should look like this:

    data class RandomResults(
        @SerializedName("recipes") val results: List<Recipe>
    )
    

    You must use the @SerializedName annotation in order for the model to be parsed. In your screenshot, I see that you get a list called "recipes", and your variable is called differently, so it can't be parsed.

    If that doesn't work, please update your question and I'll take a look again.

    Update

    Now your parser also does not work due to the wrong type of the instructions parameter. You specified the List<String>, and the API returns just a String.

    So you need to replace this line in your Recipe class:

        @SerializedName("instructions") val instructions: List<String>,
    

    with this:

        @SerializedName("instructions") val instructions: String,