flutter_simple_architecture 2.0.0
flutter_simple_architecture: ^2.0.0 copied to clipboard
A robust Flutter bootstrapping tool and project template emphasizing a clean, layer-first architecture with strict state management.
FSA (Flutter Simple Architecture) #
A robust Flutter bootstrapping tool and project template emphasizing a clean, layer-first architecture with strict state management and a centralized design system.
FSA CLI #
The project includes a CLI tool (fsa) to automate project creation and component scaffolding.
Installation #
Install from GitLab (Recommended)
To install fsa globally directly from the repository:
fvm dart pub global activate --source git https://gitlab.com/godoytristanh/fsa.git
Install from Local Source
If you have cloned the repository locally:
fvm dart pub global activate --source path .
Note: Make sure your Dart SDK's bin directory is in your system's PATH (usually ~/.pub-cache/bin).
Local Usage
If you prefer not to install it globally, you can run it directly from the root:
fvm dart bin/fsa.dart <command>
Commands #
-
Create a new project:
fsa create <project_name> <org_name>Bootstraps a new Flutter project with the FSA architecture.
- Clean Template: Removes default counter app boilerplate.
- Automated Config: Pre-configures
hooks_riverpod,freezed, andl10n. - Environment Ready: Includes
.fvmrcandanalysis_options.yaml. - Analysis Passing: Generates code that passes
flutter analyzeimmediately.
-
Generate a Page:
fsa generate page <name> [--type=async|sync|static]Creates a routeable page in
lib/presentation/<name>/.- Async (Default): Full
AppViewBuilder+Notifier+Union State. For complex, async features. - Sync:
AppViewBuilder+Notifier+Data Class. For synchronous features (forms, counters). - Static: Stateless
Scaffoldonly. For informational pages.
- Async (Default): Full
-
Generate an Async Component:
fsa generate async <path/to/name>Creates a complex component with its own independent state management. Useful for logic-heavy widgets nested within pages.
-
Generate a Dumb Component:
fsa generate dumb <path/to/name>Creates a single-file
StatelessWidgetin thewidgets/subfolder of the specified path.
Architectural Principles #
1. Page Types (Screens) #
The CLI supports three strict page archetypes. Choose the one that matches your feature's complexity.
CRITICAL RULE: Async views (and AppViewBuilder in general) MUST ONLY be used if the view performs an initial asynchronous task or calculation (e.g., fetching data from an API, loading from a database, or running a heavy computation on initialization). If a view does not require an initial async load, use Sync (for local logic) or Static (for no logic).
Async Page (Default)
- Command:
fsa generate page <Name> - Structure:
AppViewBuilder+Notifier+Union State(Initial/Loading/Success/Error). - When to use:
- EXCLUSIVELY when the view performs an initial async calculation or data fetch.
- You need to handle loading spinners, error states, or retry logic for that initial task.
- The UI has complex distinct states derived from an async operation.
Sync Page
- Command:
fsa generate page <Name> --type=sync - Structure:
AppViewBuilder+Notifier+Data Class(Sync State). - When to use:
- The view has logic/interactions but DOES NOT perform an initial async load.
- State management is synchronous and local (e.g., Form validation, Counter, Toggle settings).
- You do not need a loading spinner or terminal error handling.
Static Page
- Command:
fsa generate page <Name> --type=static - Structure:
StatelessWidget(Scaffold only). - When to use:
- Purely informational pages (About Us, Terms of Service).
- Static navigation menus.
- NO logic, NO state, NO providers (except Theme/L10n).
2. Component Types (Widgets) #
Dumb Component (Preferred)
- Command:
fsa generate dumb <path/Name> - Structure:
StatelessWidgetin awidgets/folder. - Rule:
- Must receive ALL data via constructor arguments.
- Must communicate actions via
VoidCallbackorFunction(T)callbacks. - NEVER access a Provider or Notifier directly.
Async Component
- Command:
fsa generate async <path/Name> - Structure:
AppViewBuilder+Notifier+Union State(nested folder). - Rule:
- Use ONLY when the component must manage its own independent lifecycle (e.g., a
UserAvatarthat fetches its own image by ID, independent of the page).
- Use ONLY when the component must manage its own independent lifecycle (e.g., a
3. State Management #
- Manual Riverpod Providers: We strictly use
NotifierProvider.autoDisposeandProvider.autoDisposewithout code generation. No exceptions. Use non-autoDispose only for global persistent services (e.g., Auth, Database) after careful consideration. - UI Union States: Every async component MUST use a
freezedunion class (Initial,Loading,Success,Error) to represent its UI state. - View Isolation Rule (CRITICAL):
view.dartfiles (the UI layer) ARE FORBIDDEN from usingref.watch(),ref.read(), orref.listen()of any provider. All data dependencies MUST be managed by theUIStateNotifierand exposed via theUIState. - Notifier Reactivity:
UIStateNotifieris the ONLY designated place to useref.watch()to react to changes in domain providers or repositories. - Strict Privacy:
UIStateNotifierproviders (e.g.,homeUIStateProvider) are private to their respective features and MUST ONLY be consumed by their correspondingview.dart. No other provider or component shall depend on a UI Notifier. - Clean View Rule:
view.dartfiles SHALL NOT contain private helper classes, private widgets, or methods that return widgets (e.g.,_buildItem). All sub-widgets MUST be extracted to awidgets/folder within the feature or moved tolib/core/widgetsif reusable.
4. Error Handling & Side Effects #
- Functional Programming: Powered by
fpdart. Repositories returnEither<Failure, T>. - The Guard Wrapper: Asynchronous operations are wrapped in a
guard()function. - SideEffect Notifier: Non-interactive events (dialogs, alerts) are managed via a global
sideEffectProvider. Notifiers emitSideEffectobjects (Success, Failure, Info, Warning), and theAppViewBuilderautomatically listens and displays the appropriateAppDialog.
Riverpod 3 Implementation Guide #
To ensure consistency and avoid common mistakes, follow these strict rules for Riverpod 3 within FSA:
1. Manual Providers (NO CODE GEN) #
We do not use @riverpod or build_runner for providers. Every provider must be explicitly defined using the NotifierProvider or Provider factories.
2. The Notifier Pattern (UI Layer) #
UI State is managed by a Notifier<TUIState>.
// lib/presentation/feature/ui_state_notifier.dart
class FeatureUIStateNotifier extends Notifier<FeatureUIState> {
@override
FeatureUIState build() {
// 1. Reactive dependencies go here (watch other providers)
final domainData = ref.watch(domainDataProvider);
// 2. Initial state logic
if (domainData.isEmpty) return const FeatureUIState.initial();
return FeatureUIState.success(data: domainData);
}
// 3. Actions use ref.read() for non-reactive dependencies (repositories/services)
Future<void> performAction() async {
state = const FeatureUIState.loading(); // Terminal state change
final repository = ref.read(repositoryProvider);
final result = await repository.fetchData();
result.fold(
(failure) => state = FeatureUIState.error(failure.message),
(data) => state = FeatureUIState.success(data: data),
);
}
// 4. Side effects (Snackbars/Dialogs) use sideEffectProvider
void showNotification() {
ref.read(sideEffectProvider.notifier).notify(
const SideEffect.info(title: 'Update', message: 'Action triggered!')
);
}
}
// ALWAYS use .autoDispose for UI-bound providers
final featureUIStateProvider =
NotifierProvider.autoDispose<FeatureUIStateNotifier, FeatureUIState>(() {
return FeatureUIStateNotifier();
});
3. AsyncNotifier vs Notifier #
- Prefer
Notifiercombined with a UnionUIState(Freezed) for most UI screens. This gives you explicit control overInitial,Loading,Success, andErrorstates. - Use
AsyncNotifieronly for simple data fetching where the standardAsyncValue(loading/error/data) is sufficient without custom UI transitions.
4. Ref Usage Summary #
| Location | Method | Why |
|---|---|---|
build() |
ref.watch() |
To react to changes in domain/service providers and rebuild the state. |
| Methods | ref.read() |
To access repositories or "trigger" actions on other notifiers without creating a subscription. |
UI (view.dart) |
FORBIDDEN | The UI must only interact with state and notifier provided by AppViewBuilder. |
SideEffect Pattern #
To trigger a dialog without passing a BuildContext or using callbacks:
// In your Notifier
void someAction() {
ref.read(sideEffectProvider.notifier).notify(
SideEffect.success(title: 'Done', message: 'Action completed!')
);
}
The AppViewBuilder handles this via the onSideEffect hook. By default, it shows an AppDialog, but you can override it in your Page to use SnackBars or other UI:
@override
void onSideEffect(BuildContext context, SideEffect effect) {
// Custom implementation: use a SnackBar instead of a Dialog
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(effect.message)),
);
}
5. Design System #
- Sealed Widgets: Base components (
AppButton,AppText,AppDialog) use sealed classes and factory constructors to enforce semantic consistency. - Centralized Theme: All styling is managed in
lib/core/theme/usingThemeExtension.
Project Structure #
bin/: CLI entry point.lib/core/: Global logic, services (Logger, ErrorHandler), and Design System (Sealed Widgets).lib/domain/: Pure business logic, entities, and repository definitions.lib/presentation/: UI layers, divided into Pages and nested Async Components.
Getting Started #
- Ensure FVM is installed.
- Run
fvm flutter pub get. - Generate boilerplate:
fvm flutter pub run build_runner build --delete-conflicting-outputs. - Run the app:
fvm flutter run.