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)
}
}
}
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.
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,