Creating a recipe finder using Jetpack,Kotlin

The recipe app we are building in this tutorial comes with a search functionality that allows users to find their favourite cuisines using keywords such as meal name or chef name .

· 4 min read
Wilson Ochieng

Wilson Ochieng

Android Expert developing mobile applications with 4+ Years of Experience.

topics


Recipes provide the necessary information to help people prepare different kinds of foods.This tutorial shows the steps to develop a recipe application in Jetpack and Android Studio.

Project Setup


1. Setup a new project and name it Recipe

Image

Choosing file structure

2. Next define the file structure you would wish to use.Use Model,View and Intent structure as it is very compatible with Jetpack.Add different packages to the project.Add data and inside data create model and network packages.

Image

3. Inside the ui package add screens,components and viewmodel package.

Image

Dependencies addition

4. In the build.gradle.kts file add the dependencies that will allow operations such as making network requests and also parsing json data and synchronizing to ensure that they are all downloaded.Additionally,ensure you have added internet permission in AndroidManifest.xml file.

implementation("io.ktor:ktor-client-cio-jvm:2.3.2")
implementation("io.ktor:ktor-client-content-negotiation:2.3.2")
implementation("io.ktor:kotor-serialization-kotlinx-json:2.3.2")
implementation("com.google.code.gson:gson:2.8.9")
implementation("io.coil-kt:coil-compose:2.4.0")

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Recipe"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Recipe">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
Image

Choosing API to use

This project uses mealdb free api which provides the json data.However,if you have a predefined json file just as per the requirement of the recipe challenge in this link: Recipe challenge ,the approach still remains the same.

5. Use TheMealDB free api to access recipe data that will be displayed randomly in the android app through this link :www.themealdb.com/api/json/v1/1/random.php

The json data appears like this:

{
  "meals": [
    {
      "idMeal": "52919",
      "strMeal": "Fennel Dauphinoise",
      "strDrinkAlternate": null,
      "strCategory": "Side",
      "strArea": "French",
      "strInstructions": "Heat oven to 180C/160C fan/gas 4. Put potatoes, fennel, and garlic in a medium non-stick pan. Pour in milk and double cream, season well and simmer gently, covered, for 10 mins, stirring halfway through, until potatoes are just tender.\r\nDivide the mixture between 2 small (about 150ml) buttered ramekins and scatter with Parmesan. Bake for 40 mins until the potatoes are golden and tender when pierced with a knife. Snip the reserved fennel fronds over before serving.",
      "strMealThumb": "https://www.themealdb.com/images/media/meals/ytttsv1511798734.jpg",
      "strTags": "Pie,SideDish",
      "strYoutube": "https://www.youtube.com/watch?v=tXBzZm2kkh8",
      "strIngredient1": "Potatoes",
      "strIngredient2": "Fennel",
      "strIngredient3": "Garlic",
      "strIngredient4": "Milk",
      "strIngredient5": "Double Cream",
      "strIngredient6": "Butter",
      "strIngredient7": "Parmesan Cheese",
      "strIngredient8": "",
      "strIngredient9": "",
      "strIngredient10": "",
      "strIngredient11": "",
      "strIngredient12": "",
      "strIngredient13": "",
      "strIngredient14": "",
      "strIngredient15": "",
      "strIngredient16": "",
      "strIngredient17": "",
      "strIngredient18": "",
      "strIngredient19": "",
      "strIngredient20": "",
      "strMeasure1": "225g",
      "strMeasure2": "1 small",
      "strMeasure3": "1 clove finely chopped",
      "strMeasure4": "75 ml ",
      "strMeasure5": "100ml",
      "strMeasure6": "For Greasing",
      "strMeasure7": "to serve",
      "strMeasure8": "",
      "strMeasure9": "",
      "strMeasure10": "",
      "strMeasure11": "",
      "strMeasure12": "",
      "strMeasure13": "",
      "strMeasure14": "",
      "strMeasure15": "",
      "strMeasure16": "",
      "strMeasure17": "",
      "strMeasure18": "",
      "strMeasure19": "",
      "strMeasure20": "",
      "strSource": "https://www.bbcgoodfood.com/recipes/fennel-dauphinoise",
      "strImageSource": null,
      "strCreativeCommonsConfirmed": null,
      "dateModified": null
    }
  ]
}


6. Next create a json data class inside the model package to hold the json data above.To automate this process install a plugin called JSON to Kotlin Class.Go to settings,plugins then search JSON to Kotlin Class then install.

Image

7. Copy the json data from the api endpoint,paste and then format using JSON to Kotlin that automates the process of creating a data class.In advance select Gson then name it MealResponse then click Generate.This generates two classes Meal and MealResponse.Annotate the each data class with @Serializable.

MealResponse

package com.example.recipe.data.model
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class MealResponse(
    @SerializedName("meals")
    val meals: List<Meal>
)

Meal

package com.example.recipe.data.model
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable

@Serializable
data class Meal(
    @SerializedName("dateModified")
    val dateModified: Any,
    @SerializedName("idMeal")
    val idMeal: String,
    @SerializedName("strArea")
    val strArea: String,
    @SerializedName("strCategory")
    val strCategory: String,
    @SerializedName("strCreativeCommonsConfirmed")
    val strCreativeCommonsConfirmed: Any,
    @SerializedName("strDrinkAlternate")
    val strDrinkAlternate: Any,
    @SerializedName("strImageSource")
    val strImageSource: Any,
    @SerializedName("strIngredient1")
    val strIngredient1: String,
    @SerializedName("strIngredient10")
    val strIngredient10: String,
    @SerializedName("strIngredient11")
    val strIngredient11: String,
    @SerializedName("strIngredient12")
    val strIngredient12: String,
    @SerializedName("strIngredient13")
    val strIngredient13: String,
    @SerializedName("strIngredient14")
    val strIngredient14: String,
    @SerializedName("strIngredient15")
    val strIngredient15: String,
    @SerializedName("strIngredient16")
    val strIngredient16: String,
    @SerializedName("strIngredient17")
    val strIngredient17: String,
    @SerializedName("strIngredient18")
    val strIngredient18: String,
    @SerializedName("strIngredient19")
    val strIngredient19: String,
    @SerializedName("strIngredient2")
    val strIngredient2: String,
    @SerializedName("strIngredient20")
    val strIngredient20: String,
    @SerializedName("strIngredient3")
    val strIngredient3: String,
    @SerializedName("strIngredient4")
    val strIngredient4: String,
    @SerializedName("strIngredient5")
    val strIngredient5: String,
    @SerializedName("strIngredient6")
    val strIngredient6: String,
    @SerializedName("strIngredient7")
    val strIngredient7: String,
    @SerializedName("strIngredient8")
    val strIngredient8: String,
    @SerializedName("strIngredient9")
    val strIngredient9: String,
    @SerializedName("strInstructions")
    val strInstructions: String,
    @SerializedName("strMeal")
    val strMeal: String,
    @SerializedName("strMealThumb")
    val strMealThumb: String,
    @SerializedName("strMeasure1")
    val strMeasure1: String,
    @SerializedName("strMeasure10")
    val strMeasure10: String,
    @SerializedName("strMeasure11")
    val strMeasure11: String,
    @SerializedName("strMeasure12")
    val strMeasure12: String,
    @SerializedName("strMeasure13")
    val strMeasure13: String,
    @SerializedName("strMeasure14")
    val strMeasure14: String,
    @SerializedName("strMeasure15")
    val strMeasure15: String,
    @SerializedName("strMeasure16")
    val strMeasure16: String,
    @SerializedName("strMeasure17")
    val strMeasure17: String,
    @SerializedName("strMeasure18")
    val strMeasure18: String,
    @SerializedName("strMeasure19")
    val strMeasure19: String,
    @SerializedName("strMeasure2")
    val strMeasure2: String,
    @SerializedName("strMeasure20")
    val strMeasure20: String,
    @SerializedName("strMeasure3")
    val strMeasure3: String,
    @SerializedName("strMeasure4")
    val strMeasure4: String,
    @SerializedName("strMeasure5")
    val strMeasure5: String,
    @SerializedName("strMeasure6")
    val strMeasure6: String,
    @SerializedName("strMeasure7")
    val strMeasure7: String,
    @SerializedName("strMeasure8")
    val strMeasure8: String,
    @SerializedName("strMeasure9")
    val strMeasure9: String,
    @SerializedName("strSource")
    val strSource: String,
    @SerializedName("strTags")
    val strTags: Any,
    @SerializedName("strYoutube")
    val strYoutube: String
)


8. Add sealed classes in this viewmodel package to handle data from the data classes.Name them RecipeViewIntent,RecipeViewModel and RecipeViewState.Inside the RecipeViewIntent add an object to load recipeIntent and data class to search recipes.

package com.example.recipe.ui.viewmodel
import com.example.recipe.data.model.Meal
sealed class RecipeViewState {
    object Loading: RecipeViewState()
    data class Success(val recipes: List<Meal>): RecipeViewState()
    data class Error(val message: String): RecipeViewState()
}

Making network Requests

9. Inside the network package add an api client that will help in making network requests.Name it MealApiClient.Add and object MealApiClient with a private scope that assigns val apiClient to HttpClient(CIO) enabling fetching and querying the endpoints.Additionally,it will have the logic to fetch meals from the api https://www.themealdb.com/api/json/v1/1/random.php

and to query the meals in https://www.themealdb.com/api/json/v1/1/search.php?s=$query

package com.example.recipe.data.network
import com.example.recipe.data.model.Meal
import com.example.recipe.data.model.MealResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json

object  MealApiClient {
    private val apiClient = HttpClient(CIO){
        install(ContentNegotiation) {
            json()
        }
    }

    suspend fun getRandomRecipe(): List<Meal> {
        val url = "https://www.themealdb.com/api/json/v1/1/random.php"
        val response = apiClient.get(url).body() as MealResponse
        return response.meals
    }

    suspend fun getSearchedRecipe(query: String): List<Meal> {
        val url = "https://www.themealdb.com/api/json/v1/1/search.php?s=$query"
        val response = apiClient.get(url).body() as MealResponse
        return response.meals
    }

}

10. The RecipeViewModel should have mutableStateof that monitors changes in the Intents.Also as call viewModel from lifecycle viewModel.Create two functions loadRandomRecipe and searchRecipe that are called when RecipeViewIntent is triggered depending on state of the intent.

package com.example.recipe.ui.viewmodel
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.recipe.data.network.MealApiClient
import kotlinx.coroutines.launch

class RecipeViewModel: ViewModel() {
 private val _state = mutableStateOf<RecipeViewState>(RecipeViewState.Loading)
 val state: State<RecipeViewState> = _state

 fun processIntent(intent: RecipeViewIntent) {
  when(intent) {
   is RecipeViewIntent.LoadingRandomRecipe-> loadRandomRecipe()
   is RecipeViewIntent.SearchRecipes -> searchRecipe(intent.query)
  }
 }

 private fun loadRandomRecipe() {
  viewModelScope.launch {
   _state.value = RecipeViewState.Loading
   try {
    _state.value = RecipeViewState.Success(
     MealApiClient.getRandomRecipe()
    )
   } catch(e: Exception) {
    _state.value = RecipeViewState.Error("Error fetching recipe")
   }
  }
 }

 private fun searchRecipe(query: String) {
  viewModelScope.launch {
   _state.value = RecipeViewState.Loading
   try {
    _state.value = RecipeViewState.Success(
     MealApiClient.getSearchedRecipe(query)
    )
   } catch (e: Exception) {
    _state.value = RecipeViewState.Error("Error fetching recipes")
   }
  }
 }

}

Defining the UI

11. To define the UI remove all the default composables for Hello World in the MainActivity.kt and add the RecipeViewModel created previously.

package com.example.recipe
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.recipe.ui.theme.RecipeTheme
import com.example.recipe.ui.viewmodel.RecipeViewModel

class MainActivity : ComponentActivity() {
    private val recipeViewModel: RecipeViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RecipeTheme {
                // A surface container using the 'background' color from the theme
                Surface(package com.example.recipe

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.recipe.ui.theme.RecipeTheme
import com.example.recipe.ui.viewmodel.RecipeViewModel

class MainActivity : ComponentActivity() {
    private val recipeViewModel: RecipeViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RecipeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {

                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    RecipeTheme {
        Greeting("Android")
    }
}
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {

                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    RecipeTheme {
        Greeting("Android")
    }
}


12. Next,create a composable HomeScreen in the screens package and pass recipeViewModel as a parameter.In the MainActivity.kt call the HomeScreen.

HomeScreen.kt


package com.example.recipe.ui.screens
import androidx.compose.runtime.Composable
import com.example.recipe.ui.viewmodel.RecipeViewModel
@Composable

fun HomeScreen (recipeViewModel: RecipeViewModel){

}

MainActivity.kt

package com.yourssohail.recipefinderapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.yourssohail.recipefinderapp.ui.screens.HomeScreen
import com.yourssohail.recipefinderapp.ui.theme.RecipeFinderAppTheme
import com.yourssohail.recipefinderapp.ui.viewmodel.RecipeViewModel

class MainActivity : ComponentActivity() {
    private val recipeViewModel: RecipeViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RecipeFinderAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    HomeScreen(recipeViewModel = recipeViewModel)
                }
            }
        }
    }
}

Defining components

13. Define ErrorComponent,LoadingComponent,successComponent,

searchComponent,recipeList,RecipeListItem,RecipesList in components package.LoadingComponent handles loading logic and also calls the success component.Also, ErrorComponent contains error logic while,successComponent handles the success state and displays the search portion of the UI using column composable.

ErrorComponent

package com.example.recipe.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
fun ErrorComponent(message: String, onRefreshClicked: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
      
  horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = message)
        Button(onClick = onRefreshClicked) {
            Text(text = "Refresh")
        }
    }
}


LoadingComponent

package com.example.recipe.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun LoadingComponent() {
    Column(modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Loading...")
    }
}

SuccessComponent
package com.example.recipe.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.recipe.data.model.Meal

@Composable
fun SuccessComponent(recipes: List<Meal>, onSearchClicked: (query: String) -> Unit) {
    Column {
        Text(
            text = "Recipe Finder",
            fontWeight = FontWeight(900),
            fontFamily = FontFamily.Cursive,
            fontSize = 32.sp,
            modifier = Modifier.padding(8.dp)
        )
        SearchComponent(onSearchClicked = onSearchClicked)
        RecipesList(recipes = recipes)
    }
}

SearchComponent

package com.example.recipe.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun SearchComponent(onSearchClicked: (query: String) -> Unit) {
    var query by remember { mutableStateOf("") }
    var errorMessage by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp)
    ) {
        OutlinedTextField(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.White),
            value = query,
            onValueChange = {
                if (it.isNotBlank()) {
                    errorMessage = ""
                }
                query = it
            },
            label = { Text("Search") },
            singleLine = true,
            isError = errorMessage.isNotBlank(),
            trailingIcon = {
                IconButton(
                    onClick = {
                        if (query.isNotBlank()) {
                            onSearchClicked(query)
                        } else {
                            errorMessage = "Enter a query first"
                        }
                    }
                ) {
                    Icon(
                        imageVector = Icons.Default.Search,
                        contentDescription = "Clear",
                        tint = Color.Gray
                    )
                }
            }
        )
        if (errorMessage.isNotBlank()) {
            Text(
                text = errorMessage,
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.labelSmall,
                modifier = Modifier.padding(start = 16.dp)
            )
        }
    }
}

RecipeListItem

package com.example.recipe.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import android x.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.example.recipe.data.model.Meal

@Composable
fun RecipeListItem(meal: Meal) {
    var expanded by remember { mutableStateOf(false) }

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        shape = RoundedCornerShape(4.dp),
        border = BorderStroke(1.dp, Color.LightGray),
        colors = CardDefaults.cardColors(
            Color.White
        )
    ) {
        Column(
            modifier = Modifier.padding(8.dp)
        ) {
            if (!meal.strMealThumb.isNullOrEmpty()) {
                AsyncImage(
                    model = meal.strMealThumb,
                    contentDescription = "thumbnail",
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(200.dp)
                        .clip(
                            RoundedCornerShape(8.dp)
                        )
                )
            }
            Spacer(modifier = Modifier.padding(4.dp))
            Text(
                text = meal.strMeal ?: "",
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold
            )
            Spacer(modifier = Modifier.padding(8.dp))
            Text(
                text = "Ingredients",
                fontSize = 20.sp,
                fontWeight = FontWeight.SemiBold
            )
            Text(
                text = getIngredients(meal)
            )
            Spacer(modifier = Modifier.padding(8.dp))

            AnimatedVisibility(visible = expanded) {
                Column {
                    Text(
                        text = "Instructions",
                        fontSize = 20.sp,
                        fontWeight = FontWeight.SemiBold
                    )
                    Text(
                        text = meal.strInstructions ?: ""
                    )
                }
            }
            Column(
                modifier =
                Modifier
                    .fillMaxWidth()
                    .clickable {
                        expanded = !expanded
                    }) {
                Icon(
                    imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
                    contentDescription = "Clear",
                    tint = Color.Black,
                    modifier = Modifier
                        .align(
                            Alignment.CenterHorizontally
                        )

                )
            }

        }
    }
}

fun getIngredients(meal: Meal): String {
    var ingredients = ""

    with(meal) {
        if (!strIngredient1.isNullOrEmpty()) ingredients += "$strIngredient1 - $strMeasure1\n"
        if (!strIngredient2.isNullOrEmpty()) ingredients += "$strIngredient2 - $strMeasure2\n"
        if (!strIngredient3.isNullOrEmpty()) ingredients += "$strIngredient3 - $strMeasure3\n"
        if (!strIngredient4.isNullOrEmpty()) ingredients += "$strIngredient4 - $strMeasure4\n"
        if (!strIngredient5.isNullOrEmpty()) ingredients += "$strIngredient5 - $strMeasure5\n"
        if (!strIngredient6.isNullOrEmpty()) ingredients += "$strIngredient6 - $strMeasure6\n"
        if (!strIngredient7.isNullOrEmpty()) ingredients += "$strIngredient7 - $strMeasure7\n"
        if (!strIngredient8.isNullOrEmpty()) ingredients += "$strIngredient8 - $strMeasure8\n"
        if (!strIngredient9.isNullOrEmpty()) ingredients += "$strIngredient9 - $strMeasure9\n"
        if (!strIngredient10.isNullOrEmpty()) ingredients += "$strIngredient10 - $strMeasure10\n"
        if (!strIngredient11.isNullOrEmpty()) ingredients += "$strIngredient11 - $strMeasure11\n"
        if (!strIngredient12.isNullOrEmpty()) ingredients += "$strIngredient12 - $strMeasure12\n"
        if (!strIngredient13.isNullOrEmpty()) ingredients += "$strIngredient13 - $strMeasure13\n"
        if (!strIngredient14.isNullOrEmpty()) ingredients += "$strIngredient14 - $strMeasure14\n"
        if (!strIngredient15.isNullOrEmpty()) ingredients += "$strIngredient15 - $strMeasure15\n"
        if (!strIngredient16.isNullOrEmpty()) ingredients += "$strIngredient16 - $strMeasure16\n"
        if (!strIngredient17.isNullOrEmpty()) ingredients += "$strIngredient17 - $strMeasure17\n"
        if (!strIngredient18.isNullOrEmpty()) ingredients += "$strIngredient18 - $strMeasure18\n"
        if (!strIngredient19.isNullOrEmpty()) ingredients += "$strIngredient19 - $strMeasure19\n"
        if (!strIngredient20.isNullOrEmpty()) ingredients += "$strIngredient20 - $strMeasure20\n"
    }
    return ingredients.trimEnd('\n')
}

RecipesList

package com.example.recipe.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.example.recipe.data.model.Meal

@Composable
fun RecipesList(recipes: List<Meal>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize().background(Color.White)
    ){
        items(recipes) {
            RecipeListItem(it)
        }
    }
}

The final recipe app

Image

Feel free to apply the concepts learned in this tutorial to build a recipe that has a nice user interface and offer great user experience.




share

Wilson Ochieng

Android Expert developing mobile applications with 4+ Years of Experience.