YourCookbok is a modern, offline-first Android application built to showcase a robust, scalable, and testable architecture. It allows users to search for recipes, view their details, and maintain a local collection of their favorites.
This project was developed as a technical interview showcase, prioritizing architectural quality, defensibility, and adherence to modern Android development best practices over a wide feature set.
- Search: Find recipes from TheMealDB API with a reactive, debounced search field.
- Offline-First: Search results are seamlessly supplemented with locally saved recipes when the network is unavailable.
- Favorites: Save and remove recipes to a local "favorites" list that is available offline.
- Recipe Details: View comprehensive recipe details, including ingredients, measurements, and step-by-step instructions.
- Modern UI: A clean, responsive, and intuitive user interface built entirely with Jetpack Compose and Material 3.
This project leverages a modern, industry-standard technology stack:
- UI: 100% Jetpack Compose for declarative UI development.
- Architecture: Clean Architecture + MVVM (Model-View-ViewModel).
- Dependency Injection: Hilt for managing dependencies.
- Asynchronicity: Kotlin Coroutines & Flow for all asynchronous operations.
- Networking: Retrofit for consuming TheMealDB API, with Kotlinx Serialization for JSON parsing.
- Local Database: Room for persisting favorite recipes.
- Navigation: Jetpack Compose Navigation with a type-safe, serializable routing system.
- Image Loading: Coil for efficient image loading in Compose.
- Unit Testing: JUnit5, MockK (for mocking), and Strikt (for assertions).
- UI Testing: Jetpack Compose Test Suite (
createAndroidComposeRule).
The architecture of this application was designed to be clean, modular, and highly defensible. The most important highlights summarized below.
The project follows a strict Clean Architecture pattern, separating the code into three primary layers:
- Domain: Contains the core business models (
Recipe.kt) theRecipeRepositoryinterface and UseCases that are shared across features (GetSavedRecipesUseCase,DeleteRecipeUseCase,IsRecipeSavedUseCase,SaveRecipeUseCase). This layer is pure Kotlin and has no external dependencies. - Data: Implements the
RecipeRepositoryinterface. It is responsible for all data operations, including network requests (Retrofit) and local database access (Room). It uses the DTO (Data Transfer Object) pattern to map API responses to our clean domain models. - Presentation (UI): Built with Jetpack Compose, this layer consists of ViewModels and Composable screens. ViewModels consume UseCases from the domain layer to drive the UI state.
To enhance modularity and scalability, the project uses a "package-by-feature" structure (e.g., /search, /saved, /detail). This keeps all code related to a single feature (UI, ViewModel, UseCases) in one place, making the codebase easier to navigate and maintain.
The UI layer strictly follows MVI-inspired principles:
- Single Source of Truth: The ViewModel is the sole owner of the screen's state, exposed as a
StateFlow<UiState>. - State Down, Events Up: The UI observes this state and sends all user actions to the ViewModel via a sealed
Eventclass, creating a predictable, unidirectional data flow. - Stateless Composables: All screens are implemented with a stateful "Route" composable (which connects to the ViewModel) and a stateless "Screen" composable (which only renders the UI). This makes the UI components highly testable and previewable.
The RecipeRepository is designed to be offline-first. When a user searches for a recipe, the repository first attempts to fetch fresh data from the network. If the network call fails, it automatically falls back to searching the local database of saved recipes, providing a seamless and resilient user experience.
Navigation is implemented using a fully type-safe system built on kotlinx.serialization.
- Routes as Objects: Navigation destinations are defined as
@Serializable sealed classobjects, not strings, which prevents runtime errors from typos or incorrect arguments. - Encapsulation: All navigation logic (the
NavHost,BottomNavigationBar, and route definitions) is encapsulated in a dedicated/presentation/navigationpackage, keeping theMainActivityclean and focused. - Context-Aware UI: The bottom navigation bar is context-aware, hiding itself on detail screens to provide a clearer and more intuitive navigation hierarchy.
- Clone the repository.
- Open the project in a recent version of Android Studio.
- Let Gradle sync the dependencies.
- Run the
appconfiguration on an emulator or a physical device.
- To run all unit tests, execute the
./gradlew testDebugUnitTestcommand. - To run all instrumented (UI) tests, execute the
./gradlew connectedDebugAndroidTestcommand.