Recipe Compose is a portfolio mobile application originally built to learn Jetpack Compose and later redesigned into a fuller product sample. It connects recipe discovery with restaurant search and real-world navigation: users can explore meals from TheMealDB, save recipes locally, find restaurants through Yelp, inspect a destination on an interactive map, and continue into driving directions.
The project now demonstrates an incremental Compose Multiplatform migration as well as modern Compose development across state-driven UI, Ktor networking, Room persistence, navigation, responsive layouts, and platform services. Its primary experience—including onboarding, recipe discovery, search, saved dishes, nearby restaurants, location access, interactive maps, and directions—runs on Android and iOS from shared UI and state.
Watch the Android and iOS walkthrough
- Learn the core discovery, saving, and restaurant features through a focused first-run onboarding flow.
- Move between recipe discovery, search, nearby restaurants, and saved dishes from a persistent navigation bar.
- Browse recipe categories or use a compact, image-first search to find dishes by name.
- Open a complete recipe details page, follow its original source or video, and save the dish locally.
- Open saved dishes as full recipe details, remove an individual dish with confirmation, or use swipe-to-delete for quick list management.
- Discover Yelp restaurants after explicitly choosing the device's current location, remember that choice for later visits, or enter a city or ZIP code instead.
- Search the loaded area by restaurant name or cuisine.
- View a selected restaurant on an interactive Google Map on Android or native MapKit map on iOS.
- Reposition the destination marker and launch driving directions to the active pin.
- Respond to loading, error, and network-connectivity states.
- Declarative, state-driven screens built with Compose Multiplatform and Material 3.
- Unidirectional UI state exposed through
StateFlow-backed shared stores and Android ViewModels. - Lifecycle-aware Flow collection that avoids observing inactive screens.
- Explicit UI events for retries, searches, refreshes, dialogs, favorites, and navigation.
- Debounced remote search with cancellation to prevent outdated requests from controlling the UI.
- Shared Ktor clients with
kotlinx.serializationand KtorMockEnginecontract tests. - Navigation Compose routes with state passed between destinations.
- Reusable Compose components for dialogs, media cards, links, lists, and application chrome.
- A shared recipe-details page reused by Featured Dish, search results, and saved-dish destinations.
- A debug-only screen preview catalog with representative data and paired light/dark renders.
- Purpose-built editorial feeds, adaptive galleries, and compact management lists for different content types.
- A semantic Material 3 design system with coordinated light/dark palettes, typography, spacing, and shapes.
- A shared, theme-aware startup experience with consistent branding and loading feedback on Android and iOS.
- Shared screen transitions and an app-owned frame-driven progress indicator for predictable motion across platforms.
- Local-first favorites with Room KMP, platform-specific database construction, and swipe-to-delete interactions.
- Preferences DataStore for non-blocking onboarding state and retained location intent.
- External navigation handoff that follows the currently selected map marker.
- Foreground-only location access with support for approximate and precise permission.
- User-initiated permission requests, cache-first location resolution, a bounded fresh-location attempt, and manual search fallbacks.
- Build-time credential injection, redacted authorization headers, and debug-only HTTP body logging.
- Companion documentation for the UI redesign, Gradle Kotlin DSL migration, KMP assessment, and Compose Multiplatform migration path.
The restaurant workflow turns the user's current area into an actionable destination:
User chooses current location or enters an area
↓
Foreground permission when needed (approximate or precise)
↓
Nearby Yelp restaurant discovery
↓
Restaurant selection
↓
Interactive map and destination marker
↓
Optional marker adjustment
↓
Platform driving directions
Location access is requested only after the user chooses Use my location. After a successful resolution, Preferences DataStore remembers that choice so later Nearby visits can resolve the current area automatically. The operating system remains the source of truth for the actual permission grant on both platforms: if access is revoked, the app returns to its permission/manual fallback instead of treating the stored preference as authorization. The app first accepts a recent coordinate and otherwise performs a bounded fresh-location request, so it cannot remain on a location spinner indefinitely. If access is declined or coordinates are unavailable, the Nearby screen remains usable through a city or ZIP code search. Exact coordinates and location history are never persisted, and the app does not request background location.
The directions action uses either the restaurant marker or the user-adjusted marker as its destination. Android hands the destination to Google Maps-compatible navigation; iOS opens Apple Maps for driving directions. The navigation app manages the route origin after the handoff.
The repository keeps installable Android and iOS hosts around a Kotlin Multiplatform :shared library. Common code owns portable UI, state, repositories, networking, resources, and persistence contracts. Platform source sets and hosts supply lifecycle integration, storage paths, permissions, location, maps, and external navigation.
flowchart TD
AndroidHost["Android host<br/>:app"]
IOSHost["iOS SwiftUI host<br/>iosApp"]
subgraph Shared["Shared Kotlin and Compose Multiplatform module"]
Shell["Application shell and navigation"]
UI["Compose screens, components, theme, and resources"]
Stores["StateFlow stores and UI state"]
Contracts["Domain, repository, and platform contracts"]
Data["Ktor repositories, Room KMP, and DataStore"]
Shell --> UI
UI --> Stores
Stores --> Contracts
Contracts --> Data
end
AndroidHost --> Shell
IOSHost --> Shell
subgraph Remote["Remote services"]
MealDb["TheMealDB API"]
Yelp["Yelp Fusion API"]
end
Data --> MealDb
Data --> Yelp
subgraph AndroidAdapters["androidMain adapters"]
AndroidBridge["Android platform implementations"]
AndroidLocation["Fused Location Provider"]
GoogleMaps["Google Maps Compose"]
AndroidDirections["Google Maps directions"]
AndroidStorage["Android Room database path"]
AndroidBridge --> AndroidLocation
AndroidBridge --> GoogleMaps
AndroidBridge --> AndroidDirections
AndroidBridge --> AndroidStorage
end
subgraph IOSAdapters["iosMain adapters"]
IOSBridge["iOS platform implementations"]
IOSLocation["Core Location"]
MapKit["Native MapKit"]
IOSDirections["Apple Maps directions"]
IOSStorage["iOS Room database path"]
IOSBridge --> IOSLocation
IOSBridge --> MapKit
IOSBridge --> IOSDirections
IOSBridge --> IOSStorage
end
Contracts -.->|expect/actual and interfaces| AndroidBridge
Contracts -.->|expect/actual and interfaces| IOSBridge
RecipeCompose/
├── app/ Installable Android host and Android configuration
├── shared/
│ └── src/
│ ├── commonMain/ Shared app shell, Compose UI, state, domain, and data
│ ├── androidMain/ Android location, maps, storage, and back handling
│ ├── iosMain/ iOS entry point, location, MapKit, and storage
│ └── commonTest/ Shared repository, state, persistence, and contract tests
├── iosApp/ Installable SwiftUI/Xcode host
├── docs/ Migration decisions, implementation notes, and screenshots
└── gradle/ Version catalog and shared build configuration
Contributor rule of thumb: place portable UI and route behavior in shared/src/commonMain/.../presentation, keep platform-independent contracts and state models under domain, and put Ktor/Room implementations under data. Android- or iOS-specific permissions, location, maps, storage paths, and external actions belong in their platform source sets or host applications and are wired through Koin.
| Area | Implementation |
|---|---|
| User interface | Compose Multiplatform, Jetpack Compose, Material 3 |
| State and lifecycle | Shared stores, ViewModel adapters, StateFlow, lifecycle-aware collection |
| Navigation | Navigation Compose and a shared iOS application shell |
| Networking | Ktor Client, OkHttp/Darwin engines, kotlinx.serialization |
| Local persistence | Room KMP, Preferences DataStore |
| Recipe data | TheMealDB API |
| Restaurant discovery | Yelp Fusion API |
| Location and mapping | Fused Location Provider/Google Maps on Android; Core Location/MapKit on iOS |
| Dependency injection | Koin |
| Build tooling | Kotlin Multiplatform, Kotlin DSL, version catalogs, KSP, Secrets Gradle Plugin, Xcode |
- Android Studio and a compatible Android SDK.
- Xcode with the iOS simulator runtime for iOS development.
- An Apple development team configured in Xcode when running on a physical iPhone.
- A Google Maps Platform API key with Maps SDK for Android enabled.
- A Yelp Fusion API key.
-
Clone the repository:
git clone git@github.com:GetRighhttt/RecipeCompose.git cd RecipeCompose -
Add the following values to the root
local.propertiesfile:MAPS_API_KEY=your_google_maps_key YELP_API_KEY=your_yelp_fusion_key YELP_BASE_URL=https://api.yelp.com/v3/ BASE_URL=https://www.themealdb.com/api/json/v1/1/
local.propertiesis excluded from version control. The checked-inlocal.defaults.propertiessupplies non-sensitive defaults for project synchronization and keyless CI builds. -
Build the debug application:
./gradlew :app:assembleDebug
-
Run the
appconfiguration on an Android device or emulator with Google APIs. For iOS, openiosApp/iosApp.xcodeprojin Xcode or use the checked-iniosAppXcode Application configuration in Android Studio. Select a development team under Signing & Capabilities before installing on a physical iPhone.
On the first Nearby visit, choose Use my location and grant approximate or precise foreground access to load local restaurants. The permission prompt is user initiated and can be declined without blocking the feature; enter a city or ZIP code instead. A successful device-location choice is remembered for later visits, and Choose another location resets that behavior.
Local configuration is injected without committing credentials. Android receives the values through generated BuildConfig fields and manifest placeholders; the iOS framework receives the Yelp configuration from ignored build output with an optional Xcode build-setting override.
This protects the repository, not the compiled APK. For an appropriate deployment configuration:
- Restrict the Maps key to the application ID and signing-certificate fingerprint.
- Use separate development and production credentials.
- Keep real credentials out of
local.defaults.properties. - Route Yelp requests through a backend service if the credential must remain confidential.
Run the Android build, Android checks, and shared test suite with:
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintDebug :shared:allTestsCompile the shared UI for the Apple simulator target with:
./gradlew :shared:compileKotlinIosSimulatorArm64Additional engineering notes are tracked in docs/, including the cleanup audit, retained location preference implementation, theme and UI redesign, Gradle Kotlin DSL migration notes, KMP migration assessment, and Compose Multiplatform migration plan.
For visual iteration, open app/src/debug/java/com/example/recipe_app_compose/preview/ScreenPreviews.kt with the Debug build variant selected. Android Studio can render the main screens in light and dark mode without launching the app. Shared fixtures live beside it in PreviewData.kt and are excluded from release builds.
The two hosts share the same product language, responsive Compose components, theme system, and feature state while retaining native platform integrations.
First-run onboarding |
Explore and discover |
Search dishes |
These screens are rendered by the shared Compose Multiplatform UI. Core Location and MapKit remain native platform adapters behind the common feature contracts.
Shared first-run onboarding |
Explore and discover |
Search dishes |
Location-aware discovery |
Native MapKit destination |
Local saved dishes |
Shared recipe details and saved-dish management |
||
- Fork the repository and create a focused branch.
- Implement and test the change.
- Run the verification commands above.
- Open a pull request. GitHub automatically loads the repository's pull request template; complete its summary, platform-impact, verification, and screenshot sections.
Questions and feedback are welcome at stefanbusiness95@gmail.com.









