Build a Recipe App using MVVM Architecture with Kotlin in Android
Last Updated :
09 Apr, 2025
In this article, we will make a recipe app that displays a list of Italian recipes using the retrofit library and MVVM architecture. Model — View — ViewModel (MVVM) is the industry-recognized software architecture pattern that overcomes all drawbacks of MVP and MVC design patterns. MVVM suggests separating the data presentation logic(Views or UI) from the core business logic part of the application.
We will fetch data from The Meal DB website.
Layers of MVVM:
- Model: This layer is responsible for the abstraction of the data sources. Model and ViewModel work together to get and save the data.
- View: The purpose of this layer is to inform the ViewModel about the user’s action. This layer observes the ViewModel and does not contain any kind of application logic.
- ViewModel: It exposes those data streams which are relevant to the View. Moreover, it serves as a link between the Model and the View.

Step by Step Implementation
Step 1: Create a New Project in Android Studio
To create a new project in Android Studio, please refer to How to Create/Start a New Project in Android Studio.
Note that select Kotlin as the programming language.
File and Folder Structure :
Step 2: Add Required Dependencies
Navigate to Gradle Scripts > build.gradle.kts (Module :app) and add the following dependencies under the dependencies {} scope.
Kotlin
// retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// view model and livedata
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7")
// glide (image loading)
implementation ("com.github.bumptech.glide:glide:4.16.0")
Step 3: Allow Internet permission
To allow permission for internet, navigate to app > manifests > AndroidManifest.xml and add the following code under the <manifest/> tag.
XML
<uses-permission android:name="android.permission.INTERNET"/>
Step 4: Working with Data classes
Create 4 data classes for response from api. We will be using the TheMealDB api for this project. Check out the docs here. Navigate to app > kotlin+java > {package-name}, right click on the folder and select, New > Package, set the name as models and press enter. Now right click on the models folder and select, New > Kotlin Class/File, then select Data Class and set the names as Meal, MealDetail, RecipeDetailResponse, and RecipeResponse and add the following code to the files.
Meal.kt
package com.example.recipeapp.models
data class Meal(
val idMeal: String,
val strMeal: String,
val strMealThumb: String
)
MealDetail.kt
package com.example.recipeapp.models
data class MealDetail(
val dateModified: Any,
val idMeal: String,
val strArea: String?,
val strCategory: String?,
val strCreativeCommonsConfirmed: Any?,
val strDrinkAlternate: Any?,
val strImageSource: Any?,
val strIngredient1: String?,
val strIngredient10: String?,
val strIngredient11: String?,
val strIngredient12: String?,
val strIngredient13: String?,
val strIngredient14: String?,
val strIngredient15: String?,
val strIngredient16: String?,
val strIngredient17: String?,
val strIngredient18: String?,
val strIngredient19: String?,
val strIngredient2: String?,
val strIngredient20: String?,
val strIngredient3: String?,
val strIngredient4: String?,
val strIngredient5: String?,
val strIngredient6: String?,
val strIngredient7: String?,
val strIngredient8: String?,
val strIngredient9: String?,
val strInstructions: String?,
val strMeal: String?,
val strMealThumb: String?,
val strMeasure1: String?,
val strMeasure10: String?,
val strMeasure11: String?,
val strMeasure12: String?,
val strMeasure13: String?,
val strMeasure14: String?,
val strMeasure15: String?,
val strMeasure16: String?,
val strMeasure17: String?,
val strMeasure18: String?,
val strMeasure19: String?,
val strMeasure2: String?,
val strMeasure20: String?,
val strMeasure3: String?,
val strMeasure4: String?,
val strMeasure5: String?,
val strMeasure6: String?,
val strMeasure7: String?,
val strMeasure8: String?,
val strMeasure9: String?,
val strSource: String?,
val strTags: Any?,
val strYoutube: String?
)
RecipeDetailResponse.kt
package com.example.recipeapp.models
data class RecipeDetailResponse(
val meals: List<MealDetail>
)
RecipeResponse.kt
package com.example.recipeapp.models
data class RecipeResponse(
val meals: List<Meal>
)
Step 5: Working with Network calls
Now, we will making network api calls using Retrofit. To do this, create a similar package like models and name it network. Now, create two kotlin files with the names ApiClient and ApiService which are an object and an interface respectively. Add the following code to those files.
ApiClient.kt
package com.example.recipeapp.network
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
// API base URL
private const val BASE_URL = "https://www.themealdb.com/api/json/v1/1/"
// Retrofit setup
object ApiClient {
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder().build())
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
ApiService.kt
package com.example.recipeapp.network
import com.example.recipeapp.models.RecipeDetailResponse
import com.example.recipeapp.models.RecipeResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiService {
@GET("filter.php?a=Italian")
fun getRecipes(): Call<RecipeResponse>
@GET("lookup.php")
fun getRecipeDetails(@Query("i") mealId: String): Call<RecipeDetailResponse>
}
Step 6: Working with MainActivity layouts
Navigate to app > res > layout > activity_main.xml. Now, create a layout named list_item.xml for the each item in the recyclerview. Also, create a drawable file to set as the background for each list item. Navigate to the res > drawable folder and create a new file name card_bg.xml
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".ui.home.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="32dp"
tools:listitem="@layout/list_item"/>
</LinearLayout>
list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:focusable="true"
app:cardCornerRadius="28dp"
app:cardElevation="4dp"
android:clickable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:background="@drawable/card_bg">
<androidx.cardview.widget.CardView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_margin="16dp"
app:cardCornerRadius="24dp"
app:cardElevation="5dp"
>
<ImageView
android:id="@+id/mealImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@color/white"
android:scaleType="centerCrop"/>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/mealName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/white"
android:text="Meal Name"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_gravity="center_horizontal" />
</LinearLayout>
</androidx.cardview.widget.CardView>
card_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:startColor="#1B1B1B" android:endColor="#515151" android:angle="270" />
</shape>
Design UI:
Step 7: Working with Home Screen (MainActivity)
Navigate to app > kotlin+java > {package-name}, and create a package inside this folder named ui and create another package inside the ui folder with the name home. Inside this folder drag the MainActivity.kt file, and create two new Kotlin files with the name RecipeAdapter.kt and RecipeViewModel.kt. Now, add the following codes into their respective files.
MainActivity.kt
package com.example.recipeapp.ui.home
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.recipeapp.R
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private val recipeViewModel: RecipeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recyclerView)
// Observe data from the ViewModel
recipeViewModel.recipes.observe(this) { meals ->
val adapter = RecipeAdapter(this@MainActivity, meals)
recyclerView.layoutManager = GridLayoutManager(this@MainActivity, 2)
recyclerView.adapter = adapter
}
// Fetch data from the API
recipeViewModel.fetchRecipes()
}
}
RecipeAdapter.kt
package com.example.recipeapp.ui.home
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.recipeapp.R
import com.example.recipeapp.ui.details.RecipeDetailActivity
import com.example.recipeapp.models.Meal
class RecipeAdapter(
private val context: Context,
private val meals: List<Meal>
) : RecyclerView.Adapter<RecipeAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = itemView.findViewById(R.id.mealImage)
val textView: TextView = itemView.findViewById(R.id.mealName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = meals[position]
Glide.with(context).load(item.strMealThumb).into(holder.imageView)
holder.textView.text = item.strMeal
holder.itemView.setOnClickListener {
val intent = Intent(context, RecipeDetailActivity::class.java)
intent.putExtra("MEAL_ID", item.idMeal)
context.startActivity(intent)
}
}
override fun getItemCount(): Int = meals.size
}
RecipeViewModel.kt
package com.example.recipeapp.ui.home
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.recipeapp.models.Meal
import com.example.recipeapp.models.RecipeResponse
import com.example.recipeapp.network.ApiClient
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class RecipeViewModel(application: Application) : AndroidViewModel(application) {
private val _recipes = MutableLiveData<List<Meal>>()
val recipes: LiveData<List<Meal>> get() = _recipes
// Fetch the list of recipes
fun fetchRecipes() {
ApiClient.apiService.getRecipes().enqueue(object : Callback<RecipeResponse> {
override fun onResponse(call: Call<RecipeResponse>, response: Response<RecipeResponse>) {
if (response.isSuccessful) {
_recipes.value = response.body()?.meals ?: emptyList()
}
}
override fun onFailure(call: Call<RecipeResponse>, t: Throwable) {
// Handle failure
}
})
}
}
Step 8: Working with Details Screen
Navigate to the ui folder and create a package inside it with the name details and create a new activity with the name RecipeDetailActivity.kt. Now, create another kotlin class to setup the view model of the recipe details screen with the name RecipeDetailViewModel.kt. Then, add the following codes to their respective files.
RecipeDetailActivity.kt
package com.example.recipeapp.ui.details
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.example.recipeapp.R
import com.example.recipeapp.models.MealDetail
class RecipeDetailActivity : AppCompatActivity() {
private lateinit var recipeImage: ImageView
private lateinit var recipeName: TextView
private lateinit var recipeInstructions: TextView
private lateinit var recipeIngredients: TextView
private val recipeDetailViewModel: RecipeDetailViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_recipe_detail)
recipeImage = findViewById(R.id.recipeImage)
recipeName = findViewById(R.id.recipeName)
recipeInstructions = findViewById(R.id.recipeInstructions)
recipeIngredients = findViewById(R.id.recipeIngredients)
val mealId = intent.getStringExtra("MEAL_ID") ?: return
recipeDetailViewModel.fetchRecipeDetails(mealId)
recipeDetailViewModel.recipeDetails.observe(this, { meal ->
meal?.let {
recipeName.text = it.strMeal
recipeInstructions.text = it.strInstructions
recipeIngredients.text = getIngredients(it)
Glide.with(this).load(it.strMealThumb).into(recipeImage)
}
})
}
private fun getIngredients(meal: MealDetail): String {
val ingredients = mutableListOf<String>()
for (i in 1..20) {
val ingredient = meal.javaClass.getDeclaredField("strIngredient$i").apply { isAccessible = true }.get(meal) as? String
val measure = meal.javaClass.getDeclaredField("strMeasure$i").apply { isAccessible = true }.get(meal) as? String
if (!ingredient.isNullOrEmpty()) {
ingredients.add("$ingredient - $measure")
}
}
return ingredients.joinToString("\n")
}
}
RecipeDetailViewModel.kt
package com.example.recipeapp.ui.details
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.recipeapp.network.ApiClient
import com.example.recipeapp.models.MealDetail
import com.example.recipeapp.models.RecipeDetailResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class RecipeDetailViewModel(application: Application) : AndroidViewModel(application) {
private val _recipeDetails = MutableLiveData<MealDetail>()
val recipeDetails: LiveData<MealDetail> get() = _recipeDetails
fun fetchRecipeDetails(mealId: String) {
ApiClient.apiService.getRecipeDetails(mealId).enqueue(object : Callback<RecipeDetailResponse> {
override fun onResponse(call: Call<RecipeDetailResponse>, response: Response<RecipeDetailResponse>) {
if (response.isSuccessful) {
_recipeDetails.value = response.body()?.meals?.firstOrNull() // This is MealDetail now
}
}
override fun onFailure(call: Call<RecipeDetailResponse>, t: Throwable) {
// Handle failure
}
})
}
}
activity_recipe_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".ui.details.RecipeDetailActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="32dp"
android:padding="16dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="24dp"
app:cardElevation="5dp">
<ImageView
android:id="@+id/recipeImage"
android:layout_width="match_parent"
android:layout_height="250dp"
android:contentDescription="Recipe Image"
android:scaleType="centerCrop" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/recipeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:gravity="center"
android:textStyle="bold"
android:text="Recipe Name" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="16dp"
android:textStyle="bold"
android:text="Instructions" />
<TextView
android:id="@+id/recipeInstructions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginTop="16dp"
android:text="Instructions" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="16dp"
android:textStyle="bold"
android:text="Ingredients" />
<TextView
android:id="@+id/recipeIngredients"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginTop="16dp"
android:text="Ingredients" />
</LinearLayout>
</ScrollView>
Design UI:

Refer to the following github repo to get the entire code: Recipe_Android_Application
Output:
Similar Reads
Android - Build a Movie App using Retrofit and MVVM Architecture with Kotlin
Model â View â ViewModel (MVVM) is the industry-recognized software architecture pattern that overcomes all drawbacks of MVP and MVC design patterns. MVVM suggests separating the data presentation logic(Views or UI) from the core business logic part of the application. The separate code layers of M
5 min read
How to Build a Food and Their Price List Android App using MVVM Architecture with Kotlin?
In this article, we will food app that displays a list of foods and their price using the retrofit library and MVVM architecture. Model â View â ViewModel (MVVM) is the industry-recognized software architecture pattern that overcomes all drawbacks of MVP and MVC design patterns. MVVM suggests separa
5 min read
Build Modular Android App Architecture with Example
Modules are piece of code that is independently created and maintained and can be used in a different system. In Android apps modules can be a simple feature like Search or it may be purchased on demand. For eg. In gaming apps like PUBG, skins of the guns and the character are also a module. There a
4 min read
How to Build a Simple Torch App in Android using Kotlin?
Torch Application is a very basic application that every beginner level android developer should definitely try to build while learning Android. In this article, we will be creating an application in which we will simply display a toggle button to switch on and switch off the torch. Note: If you are
4 min read
Build a Chat App in Android using Kotlin
Almost all brands these days need an app with chat functionality so that users can interact with each other or connect to their customer support easily. In this article, we will show you how to build a chat app for Android devices. Throughout this tutorial, we use the most lightweight, clean, and Go
7 min read
Calendar View App in Android with Kotlin
Calendar View is seen in most travel booking applications in which the user has to select the date of the journey. For the selection of the date, this view is used. In this article, we will take a look at How to implement Calendar View within our Android application using Kotlin. A sample video is g
3 min read
Android - Build a Book Library App using Kotlin
There are so many apps that are present in the play store which provides details related to different books inside it. Book Library is considered one of the basic applications which a beginner can develop to learn some basic concepts within android such as JSON parsing, using recycler view, and othe
11 min read
Android - Update Data in API using Volley with Kotlin
Android applications use APIs to get the data from servers in android applications. With the help of APIs, we can add, read, update and delete the data from our database using APIs. We can use Volley and Retrofit for consuming data from APIs within the android application. In this article, we will t
5 min read
MVP (Model View Presenter) Architecture Pattern in Android with Example
In the initial stages of Android development, learners do write codes in such a manner that eventually creates a MainActivity class which contains all the implementation logic(real-world business logic) of the application. This approach of app development leads to Android activity gets closely coupl
12 min read
What is Clean Architecture in Android?
With the growing period of technology, it is critical to get the architecture right if you want to build a fantastic software program, which is why itâs so essential. If youâre interested in creating android apps with the help of clean architecture, then you have arrived at the right place. Here, we
5 min read