0

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)
        }
    }
}
Bartek
  • 3
  • 3

1 Answers1

0

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,
tasjapr
  • 632
  • 4
  • 13
  • Thank you, I updated my question with the fixed code, however the issue still persists. I also changed the screenshot to show the new logged result. – Bartek Jun 04 '23 at 00:02
  • @Bartek could you please to update make screenshot with full server response? Here is IllegalStateException because of `instructions` parameter, but I can't to check it due to incomplete information in the screenshot. More likely you need to update type of your `instructions` variable from `List` to just `String` – tasjapr Jun 04 '23 at 00:13
  • That was actually it ‍♂️, I get a proper response now. Really, thank you! – Bartek Jun 04 '23 at 15:33