🍽️ Build a Modern Recipe App in Android Using Jetpack Compose and Hilt (KSP)
By Sandeep Kumar | Android Development | Jetpack Compose | June 2025
If you've ever wanted to build a visually rich, scalable, and modern Android app that fetches delicious recipes from an API and displays them beautifully — you’re in for a treat!
In this tutorial, we’re going to build a Recipe App using:
✅ Jetpack Compose (for UI)
✅ Hilt with KSP (for Dependency Injection)
✅ Retrofit (for API integration)
✅ Room DB (for local offline caching)
✅ Kotlin Coroutines + Flow
✅ ViewModel + Repository Pattern
✅ Text-to-Speech support
✅ Beautiful UI with Light/Dark Mode
🚀 What You’ll Build
🔍 Searches recipes from an API
📋 Shows detailed recipe information
❤️ Lets you mark favorites
🔊 Speaks out instructions using Text-to-Speech
💾 Caches data locally using Room DB
🌙 Supports Dark Theme with Jetpack Compose
📁 Module Structure Overview
data
- RecipeApi.kt
- RecipeDao.kt
- RecipeDatabase.kt
- RecipeDto.kt
- RecipeEntity.kt
- RecipeRepository.kt
domain
- NetworkModule.kt
- Recipe.kt
- NetworkModule.kt
presentation
- RecipeViewModel.kt
- RecipeAppNav.kt
- RecipeListScreen.kt
- RecipeDetailScreen.kt
- MainActivity.kt
- DatabaseModule.kt
- RecipeApp
User types → ViewModel calls Repo → API or DB returns data → Data mapped to domain → ViewModel updates state → Compose UI shows recipes → User taps to view details
📦 Step 1: Project Setup
✅ Create a New Android Project
- Open Android Studio.
- Select "Empty Compose Activity".
- Name it: RecipeApp
- Choose Kotlin and minimum SDK API 24+.
🛠️ Add Required Dependencies in build.gradle:
Project-level
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
id ("com.google.dagger.hilt.android") version "2.56.2" apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
App-level
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
// 1️⃣ Apply Hilt plugin first
id("com.google.dagger.hilt.android")
// 2️⃣ Then apply KSP
id("com.google.devtools.ksp")}// Compose
implementation("androidx.navigation:navigation-compose:2.9.0")
// Hilt
implementation("com.google.dagger:hilt-android:2.56.2")
implementation("androidx.room:room-common:2.7.1")
ksp("com.google.dagger:hilt-compiler:2.56.2")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("androidx.room:room-runtime:2.7.1")
implementation("androidx.room:room-ktx:2.7.1")
ksp("androidx.room:room-compiler:2.7.1")
// Coil (Image loading)
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
📦 Step 2: Set Up Data Layer (data/)
🔌 API (Retrofit)
Create RecipeApi.kt – defines endpoint to fetch recipes from MealDB API.
🧩 DTO (Data Transfer Object)
Create RecipeDto.kt – maps the JSON fields from API.
🗃️ Room Database
Create RecipeEntity.kt – defines database entity.
Create RecipeDao.kt – defines DB operations (insert, search).
Create RecipeDatabase.kt – Room database class with @Database.
🔄 Data Mapping
Write extension functions to convert:
RecipeDto → RecipeEntity
RecipeEntity → Recipe (domain model)
🧠 Step 3: Domain Layer (domain/)
📄 Domain Model
Create Recipe.kt – clean Kotlin data class for recipe.
📁 Repository Interface
Create RecipeRepository.kt – interface for abstracting data source.
🔧 Repository Implementation
Implement the repository using API + DAO fallback in data layer.
💉 Step 4: Dependency Injection (Hilt Modules)
🌐 Network
Create NetworkModule.kt – provides Retrofit + RecipeApi.
💾 Database
Create DatabaseModule.kt – provides RecipeDatabase + RecipeDao.
📱 Step 5: Presentation Layer (presentation/)
🎛️ ViewModel
Create RecipeViewModel.kt:
Inject repository
Handle search query
Expose recipes list using mutableStateOf
🧭 Navigation
Create RecipeAppNav.kt:
Define navigation graph for:
list screen
detail/{id}/{name} screen
📄 Screens
Create RecipeListScreen.kt:
Shows search bar + list of recipes using LazyColumn
On click, navigates to detail screen
Create RecipeDetailScreen.kt:
Displays selected recipe's details
🚀 Step 6: Main Application
Set up MainActivity.kt:
Annotate with @AndroidEntryPoint
Initialize Compose UI with RecipeAppNav()
🎉 Final Steps
Run the app and test search feature
(Optional) Improve UI, add TTS, favorites, offline caching
Source code is below👇👇👇👇
data
RecipeApi.kt
package com.codingbihar.recipeapp.data
import retrofit2.http.GET
import retrofit2.http.Query
interface RecipeApi {
@GET("search.php")
suspend fun searchRecipe(
@Query("s") query: String
): RecipeResponse
}
RecipeDao.kt
package com.codingbihar.recipeapp.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface RecipeDao {
@Query("SELECT * FROM recipes WHERE name LIKE '%' || :query || '%'")
suspend fun search(query: String): List<RecipeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(recipes: List<RecipeEntity>)
}
RecipeDatabase.kt
package com.codingbihar.recipeapp.data
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [RecipeEntity::class], version = 1)
abstract class RecipeDatabase : RoomDatabase() {
abstract fun recipeDao(): RecipeDao
}
RecipeDto.kt
package com.codingbihar.recipeapp.data
import com.google.gson.annotations.SerializedName
data class RecipeResponse(
val meals: List<RecipeDto>?
)
data class RecipeDto(
@SerializedName("idMeal") val idMeal: String,
@SerializedName("strMeal") val strMeal: String,
@SerializedName("strMealThumb") val strMealThumb: String,
@SerializedName("strInstructions") val strInstructions: String?,
@SerializedName("strCategory") val strCategory: String?, // ✅
@SerializedName("strArea") val strArea: String?, // ✅
@SerializedName("strIngredient1") val strIngredient1: String?,
@SerializedName("strIngredient2") val strIngredient2: 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("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("strIngredient20") val strIngredient20: String?,
@SerializedName("strMeasure1") val strMeasure1: String?,
@SerializedName("strMeasure2") val strMeasure2: 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("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("strMeasure20") val strMeasure20: String?
)
RecipeEntity.kt
package com.codingbihar.recipeapp.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipes")
data class RecipeEntity(
@PrimaryKey val idMeal: String,
val name: String,
val imageUrl: String,
val instructions: String
)
RecipeRepository.kt
package com.codingbihar.recipeapp.data
import retrofit2.http.GET
import retrofit2.http.Query
interface RecipeApi {
@GET("search.php")
suspend fun searchRecipe(
@Query("s") query: String
): RecipeResponse
}
package com.codingbihar.recipeapp.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface RecipeDao {
@Query("SELECT * FROM recipes WHERE name LIKE '%' || :query || '%'")
suspend fun search(query: String): List<RecipeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(recipes: List<RecipeEntity>)
}
package com.codingbihar.recipeapp.data
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [RecipeEntity::class], version = 1)
abstract class RecipeDatabase : RoomDatabase() {
abstract fun recipeDao(): RecipeDao
}
package com.codingbihar.recipeapp.data
import com.google.gson.annotations.SerializedName
data class RecipeResponse(
val meals: List<RecipeDto>?
)
data class RecipeDto(
@SerializedName("idMeal") val idMeal: String,
@SerializedName("strMeal") val strMeal: String,
@SerializedName("strMealThumb") val strMealThumb: String,
@SerializedName("strInstructions") val strInstructions: String?,
@SerializedName("strCategory") val strCategory: String?, // ✅
@SerializedName("strArea") val strArea: String?, // ✅
@SerializedName("strIngredient1") val strIngredient1: String?,
@SerializedName("strIngredient2") val strIngredient2: 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("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("strIngredient20") val strIngredient20: String?,
@SerializedName("strMeasure1") val strMeasure1: String?,
@SerializedName("strMeasure2") val strMeasure2: 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("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("strMeasure20") val strMeasure20: String?
)
package com.codingbihar.recipeapp.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipes")
data class RecipeEntity(
@PrimaryKey val idMeal: String,
val name: String,
val imageUrl: String,
val instructions: String
)
package com.codingbihar.recipeapp.data
import com.codingbihar.recipeapp.domain.Recipe
import javax.inject.Inject
class RecipeRepository @Inject constructor(
private val api: RecipeApi,
private val dao: RecipeDao
) {
suspend fun searchRecipes(query: String): List<Recipe> {
val response = api.searchRecipe(query).meals ?: emptyList()
return response.map { dto ->
Recipe(
id = dto.idMeal,
name = dto.strMeal,
imageUrl = dto.strMealThumb,
instructions = dto.strInstructions ?: "",
ingredients = buildIngredientList(dto),
steps = dto.strInstructions
?.split(". ")?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList(),
strCategory = dto.strCategory,
area = dto.strArea
)
}
}
private fun buildIngredientList(dto: RecipeDto): List<String> {
val ingredients = mutableListOf<String>()
val ingredientFields = listOf(
dto.strIngredient1 to dto.strMeasure1,
dto.strIngredient2 to dto.strMeasure2,
dto.strIngredient3 to dto.strMeasure3,
dto.strIngredient4 to dto.strMeasure4,
dto.strIngredient5 to dto.strMeasure5,
dto.strIngredient6 to dto.strMeasure6,
dto.strIngredient7 to dto.strMeasure7,
dto.strIngredient8 to dto.strMeasure8,
dto.strIngredient9 to dto.strMeasure9,
dto.strIngredient10 to dto.strMeasure10,
dto.strIngredient11 to dto.strMeasure11,
dto.strIngredient12 to dto.strMeasure12,
dto.strIngredient13 to dto.strMeasure13,
dto.strIngredient14 to dto.strMeasure14,
dto.strIngredient15 to dto.strMeasure15,
dto.strIngredient16 to dto.strMeasure16,
dto.strIngredient17 to dto.strMeasure17,
dto.strIngredient18 to dto.strMeasure18,
dto.strIngredient19 to dto.strMeasure19,
dto.strIngredient20 to dto.strMeasure20,
)
for ((ingredient, measure) in ingredientFields) {
if (!ingredient.isNullOrBlank()) {
val text = if (!measure.isNullOrBlank()) "$measure $ingredient" else ingredient
ingredients.add(text.trim())
}
}
return ingredients
}
}
domain
GetRecipesUseCase
package com.codingbihar.recipeapp.domain
import com.codingbihar.recipeapp.data.RecipeRepository
import javax.inject.Inject
class GetRecipesUseCase @Inject constructor(
private val repository: RecipeRepository
) {
suspend operator fun invoke(query: String): List<Recipe> {
return repository.searchRecipes(query)
}
}
NetworkModule.kt
package com.codingbihar.recipeapp.domain
import com.codingbihar.recipeapp.data.RecipeApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRecipeApi(): RecipeApi {
return Retrofit.Builder()
.baseUrl("https://www.themealdb.com/api/json/v1/1/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RecipeApi::class.java)
}
}
Recipe.kt
package com.codingbihar.recipeapp.domain
import com.codingbihar.recipeapp.data.RecipeRepository
import javax.inject.Inject
class GetRecipesUseCase @Inject constructor(
private val repository: RecipeRepository
) {
suspend operator fun invoke(query: String): List<Recipe> {
return repository.searchRecipes(query)
}
}
package com.codingbihar.recipeapp.domain
import com.codingbihar.recipeapp.data.RecipeApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRecipeApi(): RecipeApi {
return Retrofit.Builder()
.baseUrl("https://www.themealdb.com/api/json/v1/1/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RecipeApi::class.java)
}
}
package com.codingbihar.recipeapp.domain
data class Recipe(
val id: String,
val name: String,
val imageUrl: String,
val ingredients: List<String>,
val steps: List<String>,
val strCategory: String?,
val instructions: String,
val area: String?
)
presentation
RecipeViewModel.kt
package com.codingbihar.recipeapp.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.codingbihar.recipeapp.domain.GetRecipesUseCase
import com.codingbihar.recipeapp.domain.Recipe
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RecipeViewModel @Inject constructor(
private val getRecipesUseCase: GetRecipesUseCase
) : ViewModel() {
private val _recipes = MutableStateFlow<List<Recipe>>(emptyList())
val recipes: StateFlow<List<Recipe>> = _recipes
var searchQuery = MutableStateFlow("")
init {
// 🔹 Load default recipes (like "chicken") on startup
viewModelScope.launch {
_recipes.value = getRecipesUseCase("chicken")
}
}
fun search(query: String) {
searchQuery.value = query
viewModelScope.launch {
val result = getRecipesUseCase(query)
_recipes.value = result
}
}
RecipeAppNav.kt
package com.codingbihar.recipeapp.presentation
import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
@Composable
fun RecipeAppNav() {
val navController = rememberNavController()
NavHost(navController, startDestination = "recipe_list") {
composable("recipe_list") {
RecipeListScreen(navController)
}
composable(
"recipe_detail/{id}/{name}/{imageUrl}/{instructions}/{ingredients}/{steps}/{strCategory}/{area}",
arguments = listOf(
navArgument("id") { type = NavType.StringType },
navArgument("name") { type = NavType.StringType },
navArgument("imageUrl") { type = NavType.StringType },
navArgument("instructions") { type = NavType.StringType },
navArgument("ingredients") { type = NavType.StringType },
navArgument("steps") { type = NavType.StringType },
navArgument("strCategory") { type = NavType.StringType },
navArgument("area") { type = NavType.StringType }
)
) { backStackEntry ->
val name = backStackEntry.arguments?.getString("name") ?: ""
val imageUrl = backStackEntry.arguments?.getString("imageUrl") ?: ""
val instructions = backStackEntry.arguments?.getString("instructions") ?: ""
val strCategory = backStackEntry.arguments?.getString("strCategory") ?: ""
val area = backStackEntry.arguments?.getString("area") ?: ""
val ingredientsJson = backStackEntry.arguments?.getString("ingredients") ?: "[]"
val stepsJson = backStackEntry.arguments?.getString("steps") ?: "[]"
val ingredients: List<String> =
Gson().fromJson(ingredientsJson, object : TypeToken<List<String>>() {}.type)
val steps: List<String> =
Gson().fromJson(stepsJson, object : TypeToken<List<String>>() {}.type)
RecipeDetailScreen(name, imageUrl, instructions, ingredients, steps, strCategory, area)
}
}
}
RecipeListScreen.kt
package com.codingbihar.recipeapp.presentation
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import coil.compose.rememberAsyncImagePainter
import com.codingbihar.recipeapp.domain.Recipe
import com.google.gson.Gson
@Composable
fun RecipeListScreen(navController: NavController, viewModel: RecipeViewModel = hiltViewModel()) {
val recipes by viewModel.recipes.collectAsState()
val query by viewModel.searchQuery.collectAsState()
val gson = remember { Gson() }
Column(
modifier = Modifier.systemBarsPadding()
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "Recipe App", style = MaterialTheme.typography.displayLarge)
Text("By: www.codingbihar.com", style = MaterialTheme.typography.titleLarge)
OutlinedTextField(
value = query,
onValueChange = { viewModel.search(it) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Search recipes...") }
)
Spacer(modifier = Modifier.height(16.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(recipes) { recipe ->
RecipeItem(recipe = recipe) {
val ingredientsJson = Uri.encode(gson.toJson(recipe.ingredients))
val stepsJson = Uri.encode(gson.toJson(recipe.steps))
val categoryEncoded = Uri.encode(recipe.strCategory ?: "")
val areaEncoded = Uri.encode(recipe.area ?: "")
navController.navigate(
"recipe_detail/${recipe.id}/${Uri.encode(recipe.name)}/${Uri.encode(recipe.imageUrl)}/${Uri.encode(recipe.instructions)}/$ingredientsJson/$stepsJson/$categoryEncoded/$areaEncoded"
)
}
}
}
}
}
@Composable
fun RecipeItem(recipe: Recipe, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
.clickable { onClick() },
elevation = CardDefaults.cardElevation(4.dp)
) {
Column {
Image(
painter = rememberAsyncImagePainter(recipe.imageUrl),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
contentScale = ContentScale.Crop
)
Text(
text = recipe.name,
color = androidx.compose.ui.graphics.Color.Red,
maxLines = 2,
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.titleMedium
)
}
}
}
RecipeDetailScreen.kt
package com.codingbihar.recipeapp.presentation
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.unit.dp
import coil.compose.rememberAsyncImagePainter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeDetailScreen(
name: String,
imageUrl: String,
instructions: String,
ingredients: List<String>,
steps: List<String>,
strCategory: String?,
area: String?
) {
Column (Modifier.fillMaxSize()
.systemBarsPadding()
){
Text(text = "Recipe App", style = MaterialTheme.typography.displayLarge)
Text("By: www.codingbihar.com", style = MaterialTheme.typography.titleLarge)
HorizontalDivider(Modifier.fillMaxWidth().height(4.dp), color = androidx.compose.ui.graphics.Color.Red)
Box(Modifier.fillMaxWidth().height(80.dp).padding(12.dp).border(2.dp, color = Color.Red), contentAlignment = Alignment.Center){
Text(text = name, style = MaterialTheme.typography.displayMedium)
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
item {
Image(
painter = rememberAsyncImagePainter(imageUrl),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
)
Spacer(Modifier.height(16.dp))
HorizontalDivider(Modifier.fillMaxWidth(), color = Color.Red)
Text("📂 Category: ${strCategory ?: "N/A"}", color = Color.Red, style = MaterialTheme.typography.titleMedium)
Text("🌍 Area: ${area ?: "N/A"}", color = Color.Red, style = MaterialTheme.typography.titleMedium)
HorizontalDivider(Modifier.fillMaxWidth(), color = Color.Red)
Text("🧂 Ingredients", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp))
}
items(ingredients) { ingredient ->
Text("• $ingredient", style = MaterialTheme.typography.titleSmall)
}
item {
Spacer(Modifier.height(24.dp))
Text("👨🍳 Steps", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp))
}
items(steps) { step ->
Text("• $step", style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.height(6.dp))
}
item {
Spacer(Modifier.height(24.dp))
Text("📖 Instructions", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Text(instructions, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
MainActivity.kt
package com.codingbihar.recipeapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.codingbihar.recipeapp.presentation.RecipeAppNav
import com.codingbihar.recipeapp.ui.theme.RecipeAppTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
RecipeAppTheme {
/* Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
RecipeAppNav()
}
}
}
}
DatabaseModule.kt
package com.codingbihar.recipeapp
import android.app.Application
import androidx.room.Room
import com.codingbihar.recipeapp.data.RecipeDao
import com.codingbihar.recipeapp.data.RecipeDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
fun provideDatabase(app: Application): RecipeDatabase =
Room.databaseBuilder(app, RecipeDatabase::class.java, "recipe_db").build()
@Provides
fun provideRecipeDao(db: RecipeDatabase): RecipeDao = db.recipeDao()
}
RecipeApp.kt
package com.codingbihar.recipeapp.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.codingbihar.recipeapp.domain.GetRecipesUseCase
import com.codingbihar.recipeapp.domain.Recipe
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RecipeViewModel @Inject constructor(
private val getRecipesUseCase: GetRecipesUseCase
) : ViewModel() {
private val _recipes = MutableStateFlow<List<Recipe>>(emptyList())
val recipes: StateFlow<List<Recipe>> = _recipes
var searchQuery = MutableStateFlow("")
init {
// 🔹 Load default recipes (like "chicken") on startup
viewModelScope.launch {
_recipes.value = getRecipesUseCase("chicken")
}
}
fun search(query: String) {
searchQuery.value = query
viewModelScope.launch {
val result = getRecipesUseCase(query)
_recipes.value = result
}
}
package com.codingbihar.recipeapp.presentation
import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
@Composable
fun RecipeAppNav() {
val navController = rememberNavController()
NavHost(navController, startDestination = "recipe_list") {
composable("recipe_list") {
RecipeListScreen(navController)
}
composable(
"recipe_detail/{id}/{name}/{imageUrl}/{instructions}/{ingredients}/{steps}/{strCategory}/{area}",
arguments = listOf(
navArgument("id") { type = NavType.StringType },
navArgument("name") { type = NavType.StringType },
navArgument("imageUrl") { type = NavType.StringType },
navArgument("instructions") { type = NavType.StringType },
navArgument("ingredients") { type = NavType.StringType },
navArgument("steps") { type = NavType.StringType },
navArgument("strCategory") { type = NavType.StringType },
navArgument("area") { type = NavType.StringType }
)
) { backStackEntry ->
val name = backStackEntry.arguments?.getString("name") ?: ""
val imageUrl = backStackEntry.arguments?.getString("imageUrl") ?: ""
val instructions = backStackEntry.arguments?.getString("instructions") ?: ""
val strCategory = backStackEntry.arguments?.getString("strCategory") ?: ""
val area = backStackEntry.arguments?.getString("area") ?: ""
val ingredientsJson = backStackEntry.arguments?.getString("ingredients") ?: "[]"
val stepsJson = backStackEntry.arguments?.getString("steps") ?: "[]"
val ingredients: List<String> =
Gson().fromJson(ingredientsJson, object : TypeToken<List<String>>() {}.type)
val steps: List<String> =
Gson().fromJson(stepsJson, object : TypeToken<List<String>>() {}.type)
RecipeDetailScreen(name, imageUrl, instructions, ingredients, steps, strCategory, area)
}
}
}
package com.codingbihar.recipeapp.presentation
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import coil.compose.rememberAsyncImagePainter
import com.codingbihar.recipeapp.domain.Recipe
import com.google.gson.Gson
@Composable
fun RecipeListScreen(navController: NavController, viewModel: RecipeViewModel = hiltViewModel()) {
val recipes by viewModel.recipes.collectAsState()
val query by viewModel.searchQuery.collectAsState()
val gson = remember { Gson() }
Column(
modifier = Modifier.systemBarsPadding()
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "Recipe App", style = MaterialTheme.typography.displayLarge)
Text("By: www.codingbihar.com", style = MaterialTheme.typography.titleLarge)
OutlinedTextField(
value = query,
onValueChange = { viewModel.search(it) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Search recipes...") }
)
Spacer(modifier = Modifier.height(16.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(recipes) { recipe ->
RecipeItem(recipe = recipe) {
val ingredientsJson = Uri.encode(gson.toJson(recipe.ingredients))
val stepsJson = Uri.encode(gson.toJson(recipe.steps))
val categoryEncoded = Uri.encode(recipe.strCategory ?: "")
val areaEncoded = Uri.encode(recipe.area ?: "")
navController.navigate(
"recipe_detail/${recipe.id}/${Uri.encode(recipe.name)}/${Uri.encode(recipe.imageUrl)}/${Uri.encode(recipe.instructions)}/$ingredientsJson/$stepsJson/$categoryEncoded/$areaEncoded"
)
}
}
}
}
}
@Composable
fun RecipeItem(recipe: Recipe, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
.clickable { onClick() },
elevation = CardDefaults.cardElevation(4.dp)
) {
Column {
Image(
painter = rememberAsyncImagePainter(recipe.imageUrl),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
contentScale = ContentScale.Crop
)
Text(
text = recipe.name,
color = androidx.compose.ui.graphics.Color.Red,
maxLines = 2,
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.titleMedium
)
}
}
}
package com.codingbihar.recipeapp.presentation
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.unit.dp
import coil.compose.rememberAsyncImagePainter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeDetailScreen(
name: String,
imageUrl: String,
instructions: String,
ingredients: List<String>,
steps: List<String>,
strCategory: String?,
area: String?
) {
Column (Modifier.fillMaxSize()
.systemBarsPadding()
){
Text(text = "Recipe App", style = MaterialTheme.typography.displayLarge)
Text("By: www.codingbihar.com", style = MaterialTheme.typography.titleLarge)
HorizontalDivider(Modifier.fillMaxWidth().height(4.dp), color = androidx.compose.ui.graphics.Color.Red)
Box(Modifier.fillMaxWidth().height(80.dp).padding(12.dp).border(2.dp, color = Color.Red), contentAlignment = Alignment.Center){
Text(text = name, style = MaterialTheme.typography.displayMedium)
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
item {
Image(
painter = rememberAsyncImagePainter(imageUrl),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
)
Spacer(Modifier.height(16.dp))
HorizontalDivider(Modifier.fillMaxWidth(), color = Color.Red)
Text("📂 Category: ${strCategory ?: "N/A"}", color = Color.Red, style = MaterialTheme.typography.titleMedium)
Text("🌍 Area: ${area ?: "N/A"}", color = Color.Red, style = MaterialTheme.typography.titleMedium)
HorizontalDivider(Modifier.fillMaxWidth(), color = Color.Red)
Text("🧂 Ingredients", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp))
}
items(ingredients) { ingredient ->
Text("• $ingredient", style = MaterialTheme.typography.titleSmall)
}
item {
Spacer(Modifier.height(24.dp))
Text("👨🍳 Steps", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp))
}
items(steps) { step ->
Text("• $step", style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.height(6.dp))
}
item {
Spacer(Modifier.height(24.dp))
Text("📖 Instructions", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Text(instructions, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
package com.codingbihar.recipeapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.codingbihar.recipeapp.presentation.RecipeAppNav
import com.codingbihar.recipeapp.ui.theme.RecipeAppTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
RecipeAppTheme {
/* Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
RecipeAppNav()
}
}
}
}
package com.codingbihar.recipeapp
import android.app.Application
import androidx.room.Room
import com.codingbihar.recipeapp.data.RecipeDao
import com.codingbihar.recipeapp.data.RecipeDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
fun provideDatabase(app: Application): RecipeDatabase =
Room.databaseBuilder(app, RecipeDatabase::class.java, "recipe_db").build()
@Provides
fun provideRecipeDao(db: RecipeDatabase): RecipeDao = db.recipeDao()
}
package com.codingbihar.recipeapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class RecipeApp : Application()
Meenifeest
<?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:name=".RecipeApp"
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.RecipeApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.RecipeApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>