An app that lets users save their favorite coffee images for offline viewing, built to showcase my development skills for the Very Good Ventures recruitment process. Created with the Very Good CLI 🤖.
- Getting Started
- Architecture Overview
- Folder Structure
- Layered Architecture in Detail
- Design Decisions
- Running Tests
- Working with Translations
This project contains 3 flavors:
- development
- staging
- production
To run the desired flavor either use the launch configuration in VSCode/Android Studio or use the following commands:
# Development
$ flutter run --flavor development --target lib/main_development.dart
# Staging
$ flutter run --flavor staging --target lib/main_staging.dart
# Production
$ flutter run --flavor production --target lib/main_production.dart
*Coffee App works on iOS, Android, Web, and Windows.
The app is divided into several layers and modules, each with a dedicated purpose. This architecture separates UI components, business logic, and data management, ensuring that each part of the app is independent and easily testable. The main layers and their purposes are:
- UI Layer (Views)
- Business Logic Layer (Cubit and State Management)
- Data Layer (Repositories and Models)
- Core Layer (Constants and Utilities)
The folder structure follows a clean, modular approach. Each main feature is separated by folders that represent the core app, individual features (like coffee
), and supporting files.
- app
- view
- app.dart
- app_view.dart
- coffee
- cubit
- coffee_cubit.dart
- coffee_state.dart
- exceptions
- exceptions.dart
- models
- coffee_image.dart
- repositories
- http_file_coffee_repository
- http_file_coffee_repository.dart
- models
- api_coffee_image_model.dart
- coffee_repository.dart
- view
- core
- constants.dart
- l10n
- arb
- app_en.arb
- app_pt.arb
- bootstrap.dart
- main_development.dart
- main_production.dart
- main_staging.dart
The view
folder under each module (such as app/view
and coffee/view
) contains the UI components of the app. This layer includes widgets that are designed to be lightweight and free of business logic, delegating that responsibility to the Bloc architecture.
app.dart
andapp_view.dart
set up the main structure and navigation of the app, including all theMaterial
attributes.coffee/view
includes widgets specific to the coffee module, following Clean Code principles by focusing on composability and reusability.
The cubit
folder is where the business logic and state management occur. Using the Cubit flutter_bloc
allows us to:
- Maintain a reactive and declarative approach to handling state.
- Ensure that state management is isolated, making it easier to test.
- Encourage unidirectional data flow, which keeps the state consistent across the app.
This layer includes:
coffee_cubit.dart
: Manages the business logic for coffee image fetching, caching, and updating the state.coffee_state.dart
: Defines various states for the coffee images, including loading, success, and error states.
Bloc architecture allows us to create a predictable data flow, essential for debugging, especially when working with complex UI updates or asynchronous data loading.
The Data Layer is responsible for data retrieval and manipulation. By following Dependency Inversion and Single Responsibility principles, this layer is separated from the business logic, making it easy to swap out data sources without affecting the rest of the app.
-
Repositories:
coffee_repository.dart
(abstract class): This interface defines the contract for coffee data retrieval, ensuring consistency across different implementations.http_file_coffee_repository.dart
: Implements the repository interface to fetch data from the HTTP API withhttp
package, saving the files locally usingpath_provider
package.
Repositories are a fundamental part of Clean Architecture as they abstract the data sources, allowing the app to easily switch between implementations or test with mock data.
-
Models:
coffee_image.dart
: Defines the data model for coffee images, which serves as the core entity for the coffee feature.api_coffee_image_model.dart
: Represents the API response model, which is transformed into acoffee_image
for use in the app.
Having separate models for API responses and core entities is essential to isolate the business logic from the API structure. This approach follows the Data Transfer Object (DTO) pattern, improving adaptability and future-proofing the app.
The core
folder contains globally used constants. By centralizing this information, the app becomes more maintainable and avoids hardcoding values in multiple places.
constants.dart
: Stores constant values that are used throughout the app.
The l10n
folder contains the localization files from the Very Good CLI
approach, which allow the app to support multiple languages.
.arb
files (such asapp_en.arb
andapp_pt.arb
) define the strings used in the app for English and Portuguese, following internationalization (i18n) standards, coming fromflutter_localizations
package.
The bootstrap.dart
file sets up essential configurations and initializes the app with specific setups for different environments (main_development.dart
, main_production.dart
, main_staging.dart
). This is a very good benefit of using Very Good CLI
as template to start the app.
These entry points allow the app to be built with different configurations, depending on the target environment. This setup promotes flexibility, enabling better testing, debugging, and production builds.
-
Separation of Concerns: Using Clean Architecture with Bloc ensures that the app's state management, data handling, and UI rendering are isolated. This approach allows each part of the app to be tested independently, which leads to more reliable and maintainable code.
-
Declarative State Management: Bloc provides a declarative way of handling state, making it easier to debug and reducing the chances of unpredictable state mutations.
-
Reusability: By decoupling the app’s logic and UI, widgets become more reusable, adhering to Clean Code principles and facilitating feature extensions and maintenance.
To run all unit and widget tests use the following command:
$ flutter test --coverage --test-randomize-ordering-seed random
To view the generated coverage report you can use lcov.
# Generate Coverage Report
$ genhtml coverage/lcov.info -o coverage/
# Open Coverage Report
$ open coverage/index.html
This project relies on flutter_localizations and follows the official internationalization guide for Flutter.
- To add a new localizable string, open the
app_en.arb
file atlib/l10n/arb/app_en.arb
.
{
"@@locale": "en",
"appBarTitle": "Coffee App",
"@appBarTitle": {
"description": "Text shown in the AppBar of the Coffee Page"
}
}
- Then add a new key/value and description
{
"@@locale": "en",
"appBarTitle": "Coffee App",
"@appBarTitle": {
"description": "Text shown in the AppBar of the Coffee Page"
},
"helloWorld": "Hello World",
"@helloWorld": {
"description": "Hello World Text"
}
}
- Use the new string
import 'package:coffee_app/l10n/l10n.dart';
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Text(l10n.helloWorld);
}
Update the CFBundleLocalizations
array in the Info.plist
at ios/Runner/Info.plist
to include the new locale.
...
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>pt</string>
</array>
...
- For each supported locale, add a new ARB file in
lib/l10n/arb
.
├── l10n
│ ├── arb
│ │ ├── app_en.arb
│ │ └── app_pt.arb
- Add the translated strings to each
.arb
file:
app_en.arb
{
"@@locale": "en",
"appBarTitle": "Coffee App",
"@appBarTitle": {
"description": "Text shown in the AppBar of the Coffee Page"
}
}
To use the latest translations changes, you will need to generate them:
- Generate localizations for the current project:
flutter gen-l10n --arb-dir="lib/l10n/arb"
Alternatively, run flutter run
and code generation will take place automatically.