Open In App

Build a Recipe App using MVVM Architecture with Kotlin in Android

Last Updated : 31 Oct, 2022
Improve
Improve
Like Article
Like
Save
Share
Report

In this article, we will make a recipe app that displays a list of Indian 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. A sample video is given below to get an idea about what we are going to do in this article.

The separate code layers of MVVM are:

  • 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.
MVVM Architecture

 

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.

Step 2: Add Required Dependencies

Add Retrofit dependencies in Build.gradle(app) file

Kotlin




// retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'


Add View Model and Live Data dependencies in Build.gradle(app) file

Kotlin




def lifecycle_version = "2.6.0-alpha01"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// ViewModel utilities for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-live data-ktx:$lifecycle_version")


Add Glide Dependency in Build.gradle(app) file

Kotlin




implementation 'com.github.bumptech.glide:glide:4.13.2'


Glide library helps in image processing in android.

Step 3: Enable View Binding

To enable view binding to add this code inside the android {} block in build.gradle(app) file

Kotlin




buildFeatures {
    viewBinding = true
}


Step 4: Generate data classes

Right-click on the root package and select New > Kotlin-data class from JSON. If you don’t have this plugin, go to File -> Settings -> Plugin and install JSON to Kotlin Plugin. Copy the JSON result and paste it. Give this file a suitable name after that data classes will be generated.

Kotlin




data class Meal(
    val idMeal: String,
    val strMeal: String,
    val strMealThumb: String
)


Kotlin




data class Recipe(
    val meals: List<Meal>
)


Kotlin




data class MealsByCategoryName(
    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?
)


Right Click on the Root package and create an interface MealAPi

Kotlin




interface MealApi {
  
    @GET("filter.php?")
    fun getRecipeByCountryName(@Query("a") countryName : String) : Call<Recipe>
}


Create a Retrofit Instance

Kotlin




object MealInstance {
  
        val api : MealApi by lazy {
            Retrofit.Builder()
                .baseUrl("https://www.themealdb.com/api/json/v1/1/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(MealApi::class.java)
        }
}


Step 5: Design layout Files

Navigate to the app > res > layout > activity_main.xml and add the below code to that file. Below is the code for the activity_main.xml file. Comments are added inside the code to understand the code in more detail.

XML




<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
  
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/recipe_layout">
  
    </androidx.recyclerview.widget.RecyclerView>


Since we are using recycler view we need to create a new layout file for that go to res->layout and create a new Layout resource file named recipe_layout.

recipe_layout.xml file

XML




<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:layout_marginRight="5dp"
  
    <ImageView
        android:id="@+id/recipe_image"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:scaleType="fitCenter"
        android:src="@color/teal_200">
    </ImageView>
  
    <TextView
        android:id="@+id/recipe_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/recipe_image"
        android:textSize="30dp"
        android:text="Recipe Name"
        android:textAlignment="center"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:layout_marginTop="5dp">
    </TextView>
  
</androidx.constraintlayout.widget.ConstraintLayout>


We will also require another activity which will be used to display recipe instructions. We will use a collapsing ToolBar such that instructions are visible clearly.

activity_meal.xml

XML




<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activity.MealActivity">
      
      <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height= "280dp"
        android:backgroundTint="@color/g_black">
  
  
    <com.google.android.material.appbar.CollapsingToolbarLayout
  
        android:id="@+id/collapsingToolBar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:contentScrim="@color/accent"
        app:layout_scrollFlags="scroll|snap|exitUntilCollapsed"
        app:title="Meal Name">
        
        <ImageView
            android:id="@+id/img_meal_detail"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            app:layout_collapseMode="parallax">
        </ImageView>
        
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?actionBarSize"
            app:layout_collapseMode="pin"
            app:titleTextColor="@color/white">
  
  
        </androidx.appcompat.widget.Toolbar>
    </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
  
  
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/btn_fav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/_10sdp"
        android:backgroundTint="@color/accent"
        android:src="@drawable/favorite"
        android:tint="@color/white"
        app:layout_anchor="@id/appBar"
        app:layout_anchorGravity="bottom|end" />
  
   <androidx.core.widget.NestedScrollView
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
       <androidx.constraintlayout.widget.ConstraintLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:layout_marginTop="5dp"
           android:layout_marginBottom="45dp">
           <LinearLayout
               android:id="@+id/linear_layout"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:orientation="horizontal"
               app:layout_constraintTop_toTopOf="parent"
               app:layout_constraintStart_toStartOf="parent">
               <TextView
                   android:id="@+id/tv_category"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:text="Category : Pizza"
                   android:textStyle="bold"
                   android:textColor="@color/g_black"
                   android:drawableLeft="@drawable/category"
                   android:backgroundTint="@color/g_black"
                   android:layout_weight="1">
               </TextView>
               <TextView
                   android:id="@+id/tv_area"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:text="Area : Palestine"
                   android:textStyle="bold"
                   android:textColor="@color/g_black"
                   android:drawableLeft="@drawable/area"
                   android:backgroundTint="@color/g_black"
                   android:layout_weight="1">
               </TextView>
  
           </LinearLayout>
           <TextView
               android:id="@+id/instructions"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="-Instructions:"
               android:textColor="@color/black"
               android:textStyle="bold"
               app:layout_constraintStart_toStartOf="parent"
               app:layout_constraintTop_toBottomOf="@id/linear_layout"
               android:layout_marginTop="@dimen/_10sdp"
               android:layout_marginStart="@dimen/_5sdp"
               android:textSize="@dimen/_15sdp"
               android:fontFamily="@font/myfont">
  
           </TextView>
           <TextView
               android:id="@+id/tv_instructions_steps"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:textColor="@color/black"
               app:layout_constraintStart_toStartOf="@id/instructions"
               app:layout_constraintTop_toBottomOf="@id/instructions"
               android:layout_marginTop="@dimen/_2sdp">
           </TextView>
       </androidx.constraintlayout.widget.ConstraintLayout>
   </androidx.core.widget.NestedScrollView>
     
    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:layout_anchor="@id/appBar"
        android:indeterminate="true">
  
    </com.google.android.material.progressindicator.LinearProgressIndicator>
</androidx.coordinatorlayout.widget.CoordinatorLayout>


Step 6: Create a Movie Adapter class for RecyclerView

Create a MealAdapter class and set up onClickListener on each item

Kotlin




class MealAdapter : RecyclerView.Adapter<MealAdapter.ViewHolder>() {
    
    private var listOfMeals = ArrayList<Meal>()
    private lateinit var setOnMealClickListener : SetOnMealClickListener
    fun setOnMealClickListener(setOnMealClickListener: SetOnMealClickListener){
        this.setOnMealClickListener = setOnMealClickListener
    }
    fun setMealData(listOfMeals : List<Meal>) {
        this.listOfMeals = listOfMeals as ArrayList<Meal>
         notifyDataSetChanged()
    }
    class ViewHolder(val  binding: RecipeLayoutBinding)  : RecyclerView.ViewHolder(binding.root){}
  
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            RecipeLayoutBinding.inflate(
                LayoutInflater.from(
                    parent.context
                )
            )
        )
    }
  
    override fun onBindViewHolder(holder: ViewHolder, position: Int)
    {
        Glide.with(holder.itemView).load(listOfMeals[position].strMealThumb).into(holder.binding.recipeImage)
        holder.binding.recipeName.text= listOfMeals[position].strMeal
    }
  
    override fun getItemCount(): Int {
        return listOfMeals.size
    }
    interface SetOnMealClickListener {
        fun setOnClickListener(mealsByCategoryName: MealsByCategoryName)
    }
  
}


Step 7: Create Movie View Model
Since we are using MVVM architecture we need to create a View Model Class with live data in it.

Kotlin




class RecipeViewModel : ViewModel() {
private var recipeLiveData = MutableLiveData<List<Meal>>()
    fun getMealsByCountryName(){
        MealInstance.api.getRecipeByCountryName("Indian").enqueue(object  : Callback<Recipe>{
            override fun onResponse(call: Call<Recipe>, response: Response<Recipe>) {
                response.body()?.let {  mealsList->
                    recipeLiveData.postValue(mealsList.meals)
                }
            }
  
            override fun onFailure(call: Call<Recipe>, t: Throwable) {
                  Log.i("TAG" , t.message.toString())
            }
  
        })
    }
    fun observeLiveData() : LiveData<List<Meal>> {
        return recipeLiveData
    }
}


Step 8: Write Code for UI

In the end, we will write code for the main activity and meal activity which represents our UI

MainActivity.kt file:

Kotlin




class MainActivity : AppCompatActivity() {
    private lateinit var  binding: ActivityMainBinding
    private lateinit var viewModel: RecipeViewModel
    private lateinit var  mealAdapter: MealAdapter
    companion object {
        const val MEAL_ID = "com.sangyan.easyfood.idMeal"
        const val MEAL_Name = "com.sangyan.easyfood.nameMeal"
        const val MEAL_THUMB = "com.sangyan.easyfood.thumbMeal"
    }
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
       prepareRecyclerView()
        viewModel = ViewModelProvider(this)[RecipeViewModel::class.java]
        viewModel.getMealsByCountryName()
        viewModel.observeLiveData().observe(this , Observer {  it ->
  
            mealAdapter.setMealData(it)
        })
  
        mealAdapter.setOnMealClickListener(object  : MealAdapter.SetOnMealClickListener{
            override fun setOnClickListener(mealsByCategoryName: MealsByCategoryName) {
                val intent = Intent(applicationContext, MealActivity::class.java)
                intent.putExtra(MEAL_ID, mealsByCategoryName.idMeal)
                intent.putExtra(MEAL_Name, mealsByCategoryName.strMeal)
                intent.putExtra(MEAL_THUMB, mealsByCategoryName.strMealThumb)
                startActivity(intent)
            }
        })
  
    }
    private fun prepareRecyclerView() {
        mealAdapter = MealAdapter()
        binding.recyclerView.apply {
            layoutManager = GridLayoutManager(applicationContext ,2)
            adapter = mealAdapter
        }
    }
}


MealActivity.kt file:

Kotlin




class MealActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMealBinding
    private lateinit var mealId : String
    private lateinit var mealName : String
    private lateinit var mealThumb : String
    private lateinit var mealMVVM : MealViewModel
    private lateinit var youtubeLink : String
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMealBinding.inflate(layoutInflater)
        setContentView(binding.root)
        getMealsFromCountryIntent()
        setInformationViews()
        val meadDatabase = MealDatabase.getInstance(this)
        val viewModelFactory = MealViewModelFactory(meadDatabase)
        mealMVVM = ViewModelProvider(this,viewModelFactory)[MealViewModel::class.java]
        mealMVVM.getMealDetails(mealId)
        observeMealDetailsLiveData()     
    }
  
    private var mealToSave : Meal?= null
    private fun observeMealDetailsLiveData() {
        mealMVVM.observeMealDetailsLiveData().observe(this,object : Observer<Meal>{
            override fun onChanged(t: Meal?) {
                val meal = t
                mealToSave = meal
                binding.tvCategory.text = "Category : ${meal!!.strCategory}"
                binding.tvArea.text = "Area : ${meal.strArea}"
                binding.tvInstructionsSteps.text = meal.strInstructions
            }
        })
    }
  
    private fun setInformationViews() {
        Glide.with(applicationContext)
            .load(mealThumb)
            .into(binding.imgMealDetail)
        binding.collapsingToolBar.title = mealName
        binding.collapsingToolBar.setCollapsedTitleTextColor(resources.getColor(R.color.white))
        binding.collapsingToolBar.setExpandedTitleColor(resources.getColor(R.color.white))
    }
  
    private fun getMealsFromCountryIntent() {
        mealId = intent.getStringExtra(MainActivity.MEAL_ID)!!
        mealName = intent.getStringExtra(MainActivity.MEAL_Name)!!
        mealThumb = intent.getStringExtra(MainActivity.MEAL_THUMB)!!
    }
  
}


Output:



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads