RecipeApp is a Kotlin Multiplatform app that showcases recipes from the Spoonacular API using a shared UI with Compose Multiplatform for Android and iOS.
- Home with featured and popular recipes
- Search by text (debounced) and quick chips
- Recipe details
- Favorites with local persistence
- Snackbar-based error handling
- Image loading with placeholders and loading indicators
- Dark Mode support on both Android and iOS
- Kotlin Multiplatform (KMP)
- Compose Multiplatform
- Voyager (Navigation)
- Koin (Dependency Injection)
- Ktor (Networking)
- Coil 3 (Images)
- SQLDelight (Local storage)
- Coroutines + StateFlow/Flow
- Testing: MockK, Turbine, Kotlin Test, Coroutines Test
- MVI + Unidirectional Data Flow
- Contracts per screen: State, Intent, Effect
- Shared domain/data layers across platforms
composeApp/src/commonMain/— Shared UI, ViewModels, use cases, repositorycomposeApp/src/commonTest/— Shared unit testscomposeApp/src/androidMain/— Android-specificcomposeApp/src/iosMain/— iOS-specificiosApp/— iOS launcher projectbuild.gradle.kts,settings.gradle.kts— Build configuration
- Android Studio or IntelliJ IDEA with Kotlin plugin
- JDK 11 or higher
- Xcode (for iOS development)
- Gradle 8.14.3 or higher
This project uses BuildKonfig and local.properties to securely store API keys.
- Go to the Spoonacular API page
- Create a free account
- Copy your API key from the dashboard
Add your API key to the local.properties file in the project root directory:
# API Keys - DO NOT COMMIT THIS FILE TO GIT!
API_KEY=your_spoonacular_api_key_hereImportant: The local.properties file is already in .gitignore, so it won't be committed to Git. This keeps your API key secure.
Run the following command in the terminal:
./gradlew clean buildThis command will run the BuildKonfig plugin and integrate the API key into your project.
The API key is now automatically available in your code. It's accessed via BuildKonfig in AppModule.kt:
import org.example.recipeapp.BuildKonfig
val networkModule = module {
single { createHttpClient() }
single {
SpoonacularApi(
client = get(),
apiKey = BuildKonfig.API_KEY // Automatically read from local.properties
)
}
}- The
local.propertiesfile is in.gitignore, so it won't be committed to Git - Each developer should create their own
local.propertiesfile - Never write your API key directly into code files
./gradlew :composeApp:assembleDebugor open the project in Android Studio and press the Run button.
- Open the
iosApp/iosApp.xcodeprojfile in Xcode - Select a simulator or physical device
- Press the Run button
or in the terminal:
./gradlew :composeApp:iosSimulatorArm64Binaries.frameworkThis project includes comprehensive unit tests for all layers of the application.
- Core Utilities: Result sealed class tests (13 tests)
- Domain Layer: UseCase tests for all business logic (32 tests)
- GetRandomRecipesUseCase
- SearchRecipesUseCase
- GetRecipeDetailUseCase
- AddFavoriteUseCase
- RemoveFavoriteUseCase
- ToggleFavoriteUseCase
- IsFavoriteUseCase
- ObserveFavoritesUseCase
- Data Layer: Repository implementation tests (18 tests)
- RecipeRepositoryImpl
- FavoritesRepositoryImpl
- Presentation Layer: ViewModel tests with MVI pattern (31 tests)
- HomeViewModel
- SearchViewModel
- DetailsViewModel
- FavoritesViewModel
Total: ~94 unit tests covering all critical business logic and UI state management.
Run all unit tests:
./gradlew :composeApp:testDebugUnitTestRun tests with detailed output:
./gradlew :composeApp:testDebugUnitTest --infoRun specific test class:
./gradlew :composeApp:testDebugUnitTest --tests "org.example.recipeapp.presentation.home.HomeViewModelTest"View test reports:
# Open HTML test report
xdg-open composeApp/build/reports/tests/testDebugUnitTest/index.html- MockK: Mocking framework for Kotlin
- Turbine: Testing library for Flow and StateFlow
- Kotlin Test: Kotlin's testing framework
- Coroutines Test: Testing utilities for coroutines
| Home | Search | Favorites | Details |
![]() |
![]() |
![]() |
![]() |
| Home | Search | Favorites | Details |
![]() |
![]() |
![]() |
![]() |







