A robust Flutter application designed to master the BLoC pattern and Clean Architecture principles.
Culinary Companion is a recipe discovery and cooking challenge application built with a laser focus on scalable app architecture.
As my inaugural Flutter project ("Edges-project-1"), the primary objective was not just to build a functional UI, but to strictly separate business logic from the presentation layer. By strictly adhering to the BLoC (Business Logic Component) pattern, this project demonstrates how to handle complex state flows—including asynchronous data fetching, local caching, and reactive UI updates—in a clean, testable manner.
This repository serves as a proof-of-concept for my ability to implement production-grade state management and routing in Flutter.
| Explore Screen | Recipe Details |
|---|---|
![]() |
![]() |
I carefully selected a modern tech stack to ensure type safety, immutability, and performance.
- Framework: Flutter (Dart 3)
- State Management:
flutter_bloc(Cubit flavor) for predictable state changes. - Immutability:
freezedfor generating immutable state unions and data classes. - Navigation:
auto_routefor strictly typed, declarative routing. - Networking:
diofor robust HTTP requests and interceptors. - Data Parsing:
json_serializablefor efficient JSON serialization. - Local Storage:
shared_preferences& Custom Caching logic. - Assets:
flutter_svg&google_fontsfor a polished UI.
- Explore & Discover: Browse a curated list of recipes with the ability to toggle between Grid and List layouts.
- Smart Filtering: Client-side filtering of recipes based on search queries and dynamic tags (e.g., "Breakfast", "Easy").
- Daily Challenge: A logic-driven "Recipe of the Day" computed deterministically based on the date.
- Favorites System: Persist beloved recipes locally for quick access.
- Cooking History: Track recently viewed or cooked recipes.
- Robust Error Handling: Graceful UI states for loading, errors, and empty datasets.
One of the cleanest patterns in this project is the use of computed properties on Freezed states. Instead of cluttering the UI with logic, the State itself exposes getters that derive data.
Below is a snippet from ExploreState. Notice how todaysChallenge and filteredRecipes are calculated dynamically based on the current state union, keeping the UI code purely declarative.
// lib/blocs/explore/explore_state.dart
@freezed
abstract class ExploreState with _$ExploreState {
const ExploreState._();
const factory ExploreState.initial() = _Initial;
const factory ExploreState.loading() = _Loading;
const factory ExploreState.loaded({
required List<Recipe> allRecipes,
@Default('') String searchQuery,
@Default({}) final Set<String> selectedTags,
// ... other fields
}) = _Loaded;
/// Calculates the "Today's Challenge" recipe on the fly.
/// Returns null if data isn't loaded, preventing UI crashes.
Recipe? get todaysChallenge {
return when(
initial: () => null,
loading: () => null,
loaded: (allRecipes, _, __, ___, ____) {
if (allRecipes.isEmpty) return null;
// Deterministic daily rotation logic
final dayOfYear = DateTime.now().day;
final challengeIndex = dayOfYear % allRecipes.length;
return allRecipes[challengeIndex];
},
error: (message) => null,
);
}
}The project follows a Layered Architecture, separating concerns into Data, Business Logic, and UI.
lib/
├── blocs/ # State Management (Cubits & Freezed States)
│ ├── explore/ # Logic for the Explore screen
│ ├── favourited/ # Logic for user favorites
│ └── historyy/ # Logic for tracking user history
├── data/ # Data Layer
│ ├── api/ # API Services & Interceptors
│ ├── models/ # Data Transfer Objects (DTOs) & Entities
│ └── repos/ # Repositories (Abstraction over data sources)
├── ui/ # Presentation Layer
│ ├── screens/ # Full-page screens
│ └── widgets/ # Reusable UI components
├── routes/ # AutoRoute configuration
├── theme/ # App-wide styling and themes
└── main.dart # Entry point
-
Clone the repository:
git clone [https://github.com/fadyphil/Edges-project-1.git](https://github.com/fadyphil/Edges-project-1.git) cd Edges-project-1 -
Install Dependencies:
flutter pub get
-
Run Code Generation: This project uses
build_runnerfor Freezed and AutoRoute.dart run build_runner build --delete-conflicting-outputs
-
Run the App:
flutter run
Crafted with ❤️ and Flutter.

