Welcome! π
Aexus provides a robust starter kit for building Flutter applications using Clean Architecture principles. It's designed to be scalable, maintainable, and testable, incorporating many best practices and common features needed for modern app development. Whether you're starting a new project or looking for a solid foundation, this template aims to accelerate your development process.
- Why Use This Starter Kit?
- Architecture Overview
- Key Features
- Getting Started
- Renaming the Project
- How to Add a Feature (Examples)
- Core Technologies
- Contributing
- License
Building apps that grow complex over time requires a solid architectural foundation. This starter kit leverages Clean Architecture to achieve:
- β¨ Separation of Concerns: Business logic is independent of UI, frameworks, and databases.
- π§ Maintainability: Changes in one layer (like swapping a database) have minimal impact on others.
- π§ͺ Testability: Core business logic and application use cases can be tested without UI or external dependencies.
- ποΈ Scalability: The modular structure makes it easier to add new features and manage complexity as the app grows.
- π Productivity: Includes pre-configured setup for common tasks like routing, state management, networking, and more, letting you focus on features faster.
This project follows the principles of Clean Architecture, dividing the application into distinct layers with specific responsibilities.
The codebase is organized into packages, each representing a layer:
-
packages/domains(Domain Layer):- Heart of the Application: Contains the core business logic and rules, completely independent of any UI or infrastructure details.
- Contents: Entities (like
User,Post), Value Objects, and interfaces for Repositories (contracts for data access, e.g.,AuthRepository). - Key Principle: Knows nothing about the layers outside it.
-
packages/applications(Application Layer):- Orchestrator: Contains application-specific logic (Use Cases) that coordinates the flow of data between the UI and the Domain layer.
- Contents: Use Case implementations (e.g.,
LoginUseCase,GetPostsUseCase), Data Transfer Objects (DTOs) for moving data between layers, and Mappers to convert between Domain Entities and DTOs. - Key Principle: Depends only on the
domainslayer.
-
packages/infra(Infrastructure Layer):- External Interactions: Handles all communication with the outside world β databases, network APIs, device sensors, third-party SDKs.
- Contents: Concrete implementations of Repository interfaces (e.g.,
AuthRepositoryImpl), API clients (DioClient,SupabaseClientWrapper), local/remote data source abstractions, and infrastructure-specific utilities (error handling, encryption). - Key Principle: Implements interfaces defined in
domainsand depends ondomains(and sometimesapplicationsfor DTOs). Handles the "how" of data persistence and retrieval.
-
packages/presentation(Presentation Layer):- User Interface: Manages everything the user sees and interacts with.
- Architecture: Uses a feature-based organization with Store + ViewModel pattern:
- Store: Manages state and coordinates with Application layer.
- ViewModel: Transforms data for UI consumption, handles formatting and UI logic.
- Screen/Widget: Pure UI components that consume ViewModels and user interactions.
- Organization: Code is organized by business domain rather than technical layers.
- Contents: Screens/Pages, Widgets, State Management (Stores like
AuthStore), ViewModels (LoginViewModel), UI constants, and navigation logic. - Key Principle: Depends on the
applicationslayer to trigger actions and receive data (usually via DTOs). - Detailed Architecture Documentation
-
Root
lib/Directory:- Entry Point & Globals: Contains the main application entry point (
main.dart), global service initialization (Dependency Injection, Logging, Configuration), and core shared services or constants.
- Entry Point & Globals: Contains the main application entry point (
Crucially, dependencies flow inwards:
Presentation -> Application -> Domain <- Infrastructure
The Domain layer is the center and knows nothing about the outer layers. The Infrastructure layer implements interfaces defined in the Domain layer, effectively inverting the dependency. This makes the core logic independent and replaceable.
+-------------------+ +------------------+ +----------------+ +----------------------+
| Presentation | ---> | Application | ---> | Domain | <--- | Infrastructure |
| (UI, State Mgmt) | | (Use Cases, DTOs)| | (Entities,Rules| | (DB, API, Devices) |
| depends on App | | depends on Domain| | Interfaces) | | implements Domain |
+-------------------+ +------------------+ +----------------+ +----------------------+
This starter kit comes packed with features to get you going:
- π§± Layered Architecture: Enforces Clean Architecture for maintainability and testability.
- π State Management: Predictable state management using Flutter Bloc / Cubit.
- π Dependency Injection:
get_itconfigured for easy dependency management across layers. - π§ Routing: Declarative, type-safe navigation powered by
go_router. - π Networking: Robust HTTP requests using
dio, including interceptors for logging, retries, and auth. - π Authentication: Example flow using Supabase Auth (easily adaptable).
- πΎ Local Storage:
shared_preferencesfor simple key-value pairs (settings, auth status).sembastexample structure for embedded NoSQL storage, including encryption (xxtea).
- βοΈ Remote Data: Supabase integration example (Auth uses Supabase client, Posts use Dio - adaptable).
- βοΈ Configuration Management: Environment-specific settings using
--dart-define. - π Logging: Flexible custom logging (
core/logger) with levels, formatting, and multiple output options (Console included, easy to add File, Analytics, etc.). β οΈ Error Handling: Centralized (GlobalErrorStore) and localized error handling patterns.- π¨ Theming: Dynamic light/dark theme support (
ThemeStore). - π Localization (i18n): Multi-language support using JSON files (
LanguageStore,AppLocalizations). - βοΈ Code Generation: Uses
build_runnerfor essential code generation (DI, potentially JSON serialization). - π» Cross-Platform Ready: Base structure supports iOS, Android, Web, Linux, macOS, and Windows.
Ready to dive in? Follow these steps:
- Flutter SDK installed.
- An IDE like VS Code or Android Studio with Flutter plugins.
- (For Renaming Script) A Bash-compatible shell (Linux, macOS, Git Bash/WSL on Windows).
-
Clone the Repository: Choose a name for your new project directory.
git clone https://github.com/your-username/flutter_starter-main.git your_new_project_name cd your_new_project_name(Replace
https://github.com/your-username/flutter_starter-main.gitwith the actual repository URL) -
(Recommended) Rename the Project: Use the provided script to update project identifiers. See the Renaming the Project section below for details.
-
Install Dependencies: Fetch all the required packages.
flutter pub get
-
Configure Environment:
- This project uses
--dart-definefor environment configuration rather than.envfiles. - Required environment variables:
ENV=dev|test|prod # Application environment SUPABASE_URL=your_url # Your Supabase project URL SUPABASE_ANON_KEY=your_key # Your Supabase anonymous key XXTEA_PASSWORD=your_password # Password for local database encryption - This project uses
- When running from command line:
flutter run --dart-define=ENV=dev --dart-define=SUPABASE_URL=your_url --dart-define=SUPABASE_ANON_KEY=your_key --dart-define=XXTEA_PASSWORD=your_password
- For production builds:
flutter build apk --dart-define=ENV=prod --dart-define=SUPABASE_URL=your_url --dart-define=SUPABASE_ANON_KEY=your_key --dart-define=XXTEA_PASSWORD=your_password
- Create a
launch.jsonfile in the.vscodefolder (create if it doesn't exist):{ "version": "0.2.0", "configurations": [ { "name": "Flutter Development", "request": "launch", "type": "dart", "args": [ "--dart-define=ENV=dev", "--dart-define=SUPABASE_URL=your_url", "--dart-define=SUPABASE_ANON_KEY=your_key", "--dart-define=XXTEA_PASSWORD=your_password" ] }, { "name": "Flutter Production", "request": "launch", "type": "dart", "args": [ "--dart-define=ENV=prod", "--dart-define=SUPABASE_URL=your_url", "--dart-define=SUPABASE_ANON_KEY=your_key", "--dart-define=XXTEA_PASSWORD=your_password" ] } ] } - Add
.vscode/launch.jsonto your.gitignorefile to avoid committing secrets. - For team sharing, you can create a
launch.json.examplewith placeholders instead of real values.
- Go to Run > Edit Configurations.
- Select your Flutter application configuration.
- In the Additional run args field, add:
--dart-define=ENV=dev --dart-define=SUPABASE_URL=your_url --dart-define=SUPABASE_ANON_KEY=your_key --dart-define=XXTEA_PASSWORD=your_password - For different environments, create multiple run configurations with appropriate variables.
- For secure team sharing:
- Use the Jetbrains built-in passwords safe to store sensitive values
- Create shared run configurations with placeholders using
$VARIABLE_NAME$syntax - Each developer can define their own values in Environment Variables settings
For CI/CD pipelines, securely store these values as encrypted environment variables or secrets in your CI/CD provider (GitHub Actions, CircleCI, etc.) and pass them during build steps.
-
Run Code Generation: This project relies on code generation (e.g., for
get_itdependency injection setup). Run this command once:flutter pub run build_runner build --delete-conflicting-outputs
- Development Tip: For automatic regeneration whenever you save relevant files, use the
watchcommand:flutter pub run build_runner watch --delete-conflicting-outputs
- Development Tip: For automatic regeneration whenever you save relevant files, use the
-
Run the Application: Launch the app on your desired emulator, simulator, or device.
flutter run
A helper script (rename_flutter_project.sh) is included to automate renaming the project across various configuration files (Flutter, Android, iOS, etc.).
Important: Run this script before making significant code changes.
Steps:
- Navigate to Root: Ensure you are in the root directory of your cloned project in your terminal.
- Grant Execute Permissions:
chmod +x rename_flutter_project.sh
- Run the Script:
./rename_flutter_project.sh
- Follow Prompts:
- Enter the new project name (e.g.,
my_cool_app- lowercase with underscores). - Enter the new package name (e.g.,
com.mycompany.mycoolapp- reverse domain format). - Enter the new display name (e.g.,
My Cool App- the name users see). - Carefully review the proposed changes and confirm with
yorY.
- Enter the new project name (e.g.,
Hereβs how the architecture guides feature development:
Goal: Create a screen to show the logged-in user's profile (name, email).
-
domainsLayer:- Define a
UserProfileentity (if needed, maybeUseris enough) inpackages/domains/lib/entity/user/. - Add a method contract to
UserRepository(packages/domains/lib/user/user_repository.dart- create if doesn't exist):abstract class UserRepository { Future<UserProfile> getUserProfile(String userId); // ... other user-related methods }
- Define a
-
applicationsLayer:- Create
UserProfileDTO(packages/applications/lib/dto/) if the UI needs a specific data shape. - Create
UserProfileMapper(packages/applications/lib/mapper/) if using a DTO. - Create
GetUserProfileUseCase(packages/applications/lib/usecase/user/):- Inject
UserRepository. - Implement a
call()method that takesuserId, callsuserRepository.getUserProfile(userId), maps to DTO if needed, and returns it.
- Inject
- Register
GetUserProfileUseCasewithGetIt(packages/applications/lib/di/).
- Create
-
infraLayer:- Create
UserRepositoryImpl(packages/infra/lib/adapters/repository/user/) implementingUserRepository. - Inject necessary data sources (e.g.,
SupabaseClientWrapperor a dedicatedUserApiusingDioClient). - Implement
getUserProfile: Call the backend API, handle errors, map the response to theUserProfileentity. - Register
UserRepositoryImplwithGetIt(packages/infra/lib/di/).
- Create
-
presentationLayer:- Create
ProfileScreen(packages/presentation/lib/screens/profile/). - Create
ProfileStore(Cubit/Bloc) (packages/presentation/lib/screens/profile/store/):- Inject
GetUserProfileUseCase. - Manage states:
initial,loading,success(UserProfileDTO data),error(String message). - Add a method
fetchProfile()that calls the use case and emits states accordingly.
- Inject
- Register
ProfileStorewithGetIt(packages/presentation/lib/di/). - In
ProfileScreen:- Provide/Access
ProfileStore(e.g., usingBlocProvider). - Use
BlocBuilderto display UI based on the store's state (show loader, error, or profile data).
- Provide/Access
- Add a route for
/profileinAppRouter(packages/presentation/lib/utils/app_router.dart). - Add navigation (e.g., a button in
HomeScreen) usingNavigationServiceto go to the profile screen.
- Create
Goal: Add a switch in settings to enable/disable analytics.
-
domainsLayer:- Add methods to
SettingRepository(packages/domains/lib/setting/setting_repository.dart):abstract class SettingRepository { Future<void> setAnalyticsEnabled(bool enabled); Future<bool> isAnalyticsEnabled(); // ... other settings }
- Add methods to
-
infraLayer:- Implement the new methods in
SettingRepositoryImpl(packages/infra/lib/adapters/repository/setting/). - Inject
SharedPreferenceHelper. - Use
SharedPreferenceHelperto save/retrieve the boolean value. Define a constant key inPreferences(packages/infra/lib/datasources/local/sharedpref/constants/preferences.dart). - Add corresponding methods to
SharedPreferenceHelperitself.
- Implement the new methods in
-
applicationsLayer:- Create
SetAnalyticsEnabledUseCaseandGetAnalyticsEnabledUseCase(packages/applications/lib/usecase/setting/). - Inject
SettingRepositoryinto them. - Implement
call()methods that simply invoke the repository methods. - Register the use cases with
GetIt.
- Create
-
presentationLayer:- Add state and methods to
SettingsStore(or create one) (packages/presentation/lib/store/orscreens/settings/store/).- Inject the new use cases.
- Hold the
isAnalyticsEnabledstate. - Provide a method
toggleAnalytics(bool newValue)that callsSetAnalyticsEnabledUseCaseand updates the state (potentially re-fetching withGetAnalyticsEnabledUseCase).
- In your
SettingsScreen:- Use
BlocBuilderto get the currentisAnalyticsEnabledstate from the store. - Add a
SwitchListTilewidget. - Set its
valuefrom the store's state. - In
onChanged, call thetoggleAnalyticsmethod in the store.
- Use
- (Optional) Use
GetAnalyticsEnabledUseCaseduring app startup (main.dartor splash screen logic) to conditionally initialize analytics services.
- Add state and methods to
This starter kit integrates several popular and robust libraries:
- UI Framework: Flutter
- Language: Dart
- State Management: Bloc / Cubit
- Dependency Injection: GetIt
- Routing: GoRouter
- HTTP Client: Dio
- Backend-as-a-Service: Supabase Flutter (for Auth, adaptable for DB/Storage)
- Embedded Database: Sembast (example structure included)
- Key-Value Storage: shared_preferences
- Environment Variables: flutter_dotenv
- Internationalization: intl /
AppLocalizations - Logging: Custom Logger (
core/logger) - Code Generation: build_runner
Contributions are welcome! If you find a bug, have a suggestion, or want to add a feature:
- Please check the Issues tab first to see if a similar topic exists.
- If not, feel free to open a new issue to discuss your idea or report the bug.
- For code changes, please fork the repository, create a feature branch, and submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details. Happy coding!