flutter_simple_architecture 3.0.0 copy "flutter_simple_architecture: ^3.0.0" to clipboard
flutter_simple_architecture: ^3.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. Running fsa without any arguments will start an interactive menu to guide you.

For detailed architectural standards, command deep-dives, and engineering philosophy, see the FSA CLI Engineering Specification.

Installation #

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. (Interactive if arguments are omitted).

    • Clean Template: Removes default counter app boilerplate.
    • Automated Config: Pre-configures hooks_riverpod, freezed, and l10n.
    • Environment Ready: Includes .fvmrc and analysis_options.yaml.
    • Analysis Passing: Generates code that passes flutter analyze immediately.
  • Audit project architecture:

    fsa audit
    

    Performs a high-speed scan of the codebase to identify architectural violations (e.g., direct provider access in views, missing repository guards, raw Material widget usage).

  • Generate a Page:

    fsa generate page <name> [--type=async|sync|static]
    

    Creates a routeable page in lib/presentation/<name>/. (Interactive if name is omitted).

    • Async (Default): Full AppViewBuilder + Notifier + Union State. For complex, async features.
    • Sync: AppViewBuilder + Notifier + Data Class. For synchronous features (forms, counters).
    • Static: Stateless Scaffold only. For informational pages.
  • Generate a Smart Component:

    fsa generate smart <path/to/name> [--type=async|sync]
    

    Creates a complex component with its own independent state management. Useful for logic-heavy widgets nested within pages. (Interactive if path is omitted).

  • Generate a Dumb Component:

    fsa generate dumb <path/to/name>
    

    Creates a single-file StatelessWidget in the widgets/ subfolder of the specified path. (Interactive if path is omitted).

  • Generate a Repository:

    fsa generate repository <name>
    

    Creates a new repository class in lib/domain/repositories/ that extends BaseRepository.

  • Generate an Entity:

    fsa generate entity <name>
    

    Creates a new freezed data class in lib/domain/entities/.

  • Generate a Localization Key:

    fsa generate l10n <key> <value>
    

    Appends a key-value pair to app_en.arb and triggers generation.

Running fsa generate with no subcommands will trigger an interactive selection menu.


Why Layer-First? (Architectural Philosophy) #

FSA makes a deliberate, opinionated choice: Layer-First Architecture over Feature-First.

The Problem with Feature-First (The Subjective Trap) #

In Feature-First architecture (e.g., features/login/, features/profile/), boundaries are subjective. Teams often waste hours in "Architectural Debates":

  • "Does this logic belong to the Login feature or the Auth service?"
  • "Where do I put a widget that is used by both Profile and Settings?"
  • "We have a circular dependency because Feature A needs a repository from Feature B."

This leads to Feature Fragmentation, where business logic is scattered and duplicated, making the project harder to audit and maintain as it scales.

The Power of Layer-First (The Objective Foundation) #

Layer-First is objective. There is zero ambiguity about where a file belongs:

  • A Data Contract is in domain/repositories.
  • A UI Screen is in presentation/.
  • A Network Interceptor is in core/network.

This ensures a Standardized Dependency Flow: UI -> Domain -> Data. It forces developers to think about the application as a unified system rather than a collection of isolated islands.


Solving the "Dev UX" Problem #

The #1 complaint about Layer-First architecture is the Developer Experience (Dev UX):

  1. Boilerplate Fatigue: Creating 5 files in 3 different directories just to add one screen.
  2. Navigation Friction: Constant jumping between deeply nested folders.

The FSA Solution: CLI Automation #

FSA solves this by using the fsa CLI to bridge the gap. We provide the Speed of Feature-First with the Integrity of Layer-First.

Feature-First (Subjective) Standard Layer-First (Tedious) FSA (CLI + Layered)
Fast start, messy end. Slow start, clean end. Fast start, clean end.
Ambiguous boundaries. Clear boundaries. Clear boundaries.
Manual wiring. Manual wiring. Automated wiring via CLI.

Example: The 1-Second Vertical Slice

To add a new "Profile" screen, you don't manually create folders and files. You run:

fsa generate page profile

The CLI instantly scaffolds the entire vertical slice across layers:

  1. lib/presentation/profile/view.dart (UI Layer)
  2. lib/presentation/profile/ui_state.dart (State Layer)
  3. lib/presentation/profile/ui_state_notifier.dart (Logic Layer)

By automating the "Manual Labor" of layering, FSA allows developers to focus 100% on Business Logic while the architecture remains Strict, Objective, and Industrial-Grade.


Observability & Security #

1. Secure Session Observability #

FSA includes a "Black Box" logging system. The LoggerService maintains an in-memory buffer of the last 100 Breadcrumbs (Sanitized events like State updates, Network requests, and Navigation).

  • Data Scrubbing: All logs pass through LogSanitizer, which recursively redacts sensitive keys (e.g., password, token, email).
  • Crash Autopsy: When an uncaught exception occurs, FSA automatically prints a Secure Crash Report containing the exception, stack trace, and the sanitized session timeline.

2. Environment Configuration #

Manage different environments using --dart-define=ENV=dev|stg|prod.

  • Provider: Access via envProvider.
  • Config: Defined in lib/core/config/env.dart.

3. Persistent Storage #

A unified StorageService provides type-safe access to:

  • SharedPreferences: For non-sensitive settings (Theme, Onboarding).
  • SecureStorage: For sensitive data (Auth tokens).

4. Connectivity Monitoring #

The ConnectivityService automatically monitors internet status. If the connection drops, it emits a global SideEffect.warning to notify the user instantly on any screen.

5. App Bootstrap Orchestrator #

To prevent the "White Screen of Death" and ensure high polish, FSA uses a BootstrapService and SplashView.

  • Sequence: Automatically initializes StorageService, ApiClient, and Environment before the user enters the app.
  • Silent Login: The bootstrap sequence includes a trySilentLogin() check to automatically restore the user's session if a valid token exists.
  • Fail-Safe: If a critical service fails to initialize, the SplashView displays a detailed error and a retry mechanism.

Core Infrastructure Pillars #

1. Authentication Blueprint #

FSA provides a pre-wired, secure Authentication flow out of the box.

  • Session Management: Handled via authProvider. It manages initial, unauthenticated, and authenticated Case states.
  • Auto-Injection: The AuthInterceptor automatically attaches the Bearer token to every Dio request if the user is logged in.
  • Security: Auth tokens are persisted using StorageService (utilizing flutter_secure_storage for sensitive data).
  • Auto-Logout: If any API request returns a 401 Unauthorized, the interceptor triggers an automatic global logout and redirects to the login flow.

2. Infinite Pagination Mixin #

Standardize large list handling using the AppPaginationMixin<T, State>.

  • Features: Automates pageNumber, hasMore, and isLoadingMore logic.
  • Usage: Mix it into any Notifier to instantly add pagination capabilities to your state.

3. Persistent Theme Orchestrator #

Allow users to switch between Light, Dark, and System modes with persistence.

  • Provider: Access via themeProvider.
  • Persistence: User preferences are automatically saved to StorageService and restored on app launch.

4. Semantic Form Validation (DSL) #

Upgrade your form logic with a declarative Validation DSL.

  • Rules: Define rules like ValidationRule.required(), ValidationRule.email(), or ValidationRule.minLength(8).
  • Integration: AppFormMixin automatically scans your rules, validates fields, and updates the UIState with field-specific error messages.

UI & Layout #

1. Adaptive Layout System #

FSA provides a standardized way to handle Mobile, Tablet, and Desktop breakpoints.

  • AppResponsive Widget: A layout builder that automatically switches between mobile, tablet, and desktop versions of a screen.
  • ScreenType Extensions: Accessible via context.screenType or context.isMobile/isTablet/isDesktop.

2. Standardized Skeleton/Shimmer #

Every AppViewBuilder supports a buildLoading method. By utilizing the built-in Shimmer and SkeletonBox widgets, you can create branded skeleton UIs that automatically inherit the app's theme.


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 + Data Class with Status Enum.
  • 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.
    • You want to preserve existing data while refreshing (e.g., keeping the list visible while a new fetch is in progress).

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: StatelessWidget or HookWidget in a widgets/ folder.
  • Rule:
    • Must receive ALL data via constructor arguments.
    • Must communicate actions via VoidCallback or Function(T) callbacks.
    • NEVER access a Provider or Notifier directly.
    • Hooks Policy: Dumb components are the ONLY designated place for flutter_hooks (e.g., useTextEditingController, useTabController, or useAnimationController) to manage local widget-level lifecycle state.

Smart Component

  • Command: fsa generate smart <path/Name> [--type=async|sync]
  • Structure: AppViewBuilder + Notifier + State (nested folder).
  • Rule:
    • Use ONLY when the component must manage its own independent lifecycle (e.g., a UserAvatar that fetches its own image by ID, independent of the page).
    • Async: Uses Data Class with Status Enum.
    • Sync: Uses Data Class for local state.

3. State Management #

  • Manual Riverpod Providers: We strictly use NotifierProvider.autoDispose and Provider.autoDispose without code generation. No exceptions. Use non-autoDispose only for global persistent services (e.g., Auth, Database) after careful consideration.
  • UI States: Every async component MUST use a freezed data class with a Status Enum (e.g., initial, loading, success, error) to represent its UI state. This prevents data loss when transitioning between states (e.g., you can keep todos while the status is loading).
  • Data Class Pattern: We use freezed with an abstract class for all state objects and domain entities.
  • View Isolation Rule (CRITICAL): view.dart files (the UI layer) ARE FORBIDDEN from using ref.watch(), ref.read(), or ref.listen() of any provider. All data dependencies MUST be managed by the UIStateNotifier and exposed via the UIState.
  • No Hooks in Views: Files extending AppViewBuilder SHALL NOT use flutter_hooks. All initialization logic must be handled in the Notifier's build() method or via an explicit load() action triggered by the router/parent.
  • Notifier Reactivity: UIStateNotifier is the ONLY designated place to use ref.watch() to react to changes in domain providers or repositories.
  • Strict Privacy: UIStateNotifier providers (e.g., homeUIStateProvider) are private to their respective features and MUST ONLY be consumed by their corresponding view.dart. No other provider or component shall depend on a UI Notifier.
  • Clean View Rule: view.dart files SHALL NOT contain private helper classes, private widgets, or methods that return widgets (e.g., _buildItem). All sub-widgets MUST be extracted to a widgets/ folder within the feature or moved to lib/core/widgets if reusable.

4. Error Handling & Side Effects #

  • Functional Programming: Powered by fpdart. Repositories return Either<Failure, T>.
  • The Guard Wrapper: Asynchronous operations are wrapped in a guard() function.
  • Transactions & Rollback: Multi-step operations (e.g., File Upload + Database Save) utilize runTransaction([...]) to ensure atomicity. If a step fails, all previous steps are rolled back in reverse order.
  • Repository Caching: The BaseRepository provides fetchWithCache<T>, which implements a robust Cache-Then-Network strategy, allowing for offline-first capabilities with zero extra boilerplate.

Global Side-Effect Management #

Non-interactive events (Success dialogs, Error alerts, etc.) are managed via a centralized, context-less system.

  1. The Trigger: Notifiers emit a SideEffect using the sideEffectProvider:
    ref.read(sideEffectProvider.notifier).notify(
      const SideEffect.success(title: 'Done', message: 'Task added!')
    );
    
  2. The Handler: The root MyApp widget (lib/main.dart) listens to this provider globally.
  3. The Display: When an effect is emitted, main.dart uses the NavigationService to find the current context and displays the appropriate AppDialog automatically.

This ensures that developers never have to manually manage showDialog() or BuildContext for standard alerts, and it prevents duplicate dialogs from appearing when multiple components are active.


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(status: FeatureUIStatus.initial);
    return FeatureUIState(status: FeatureUIStatus.success, data: domainData);
  }

  // 3. Actions use ref.read() for non-reactive dependencies (repositories/services)
  Future<void> performAction() async {
    state = state.copyWith(status: FeatureUIStatus.loading); // Terminal state change
    
    final repository = ref.read(repositoryProvider);
    final result = await repository.fetchData();

    result.fold(
      (failure) => state = state.copyWith(
        status: FeatureUIStatus.error,
        errorMessage: failure.message,
      ),
      (data) => state = state.copyWith(
        status: FeatureUIStatus.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 Notifier combined with a Data Class UIState (Freezed) and a Status Enum for most UI screens. This gives you explicit control over initial, loading, success, and error states while preserving data across transitions.
  • Use AsyncNotifier only for simple data fetching where the standard AsyncValue (loading/error/data) is sufficient without custom UI transitions or the need to preserve complex state during refreshes.

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.

5. Design System #

  • Sealed Widgets: Base components (AppButton, AppText, AppDialog, AppTextField, AppChip) use sealed classes and factory constructors to enforce semantic consistency.
  • Pure Inheritance: Widgets are designed as "Naked Shells" that return standard Material components (e.g., ElevatedButton, TextField, RawChip). They automatically inherit 100% of their styling from the global ThemeData defined in lib/core/theme/theme.dart.
  • Zero Manual Coding: Apart from Typography variants, build methods do not contain Theme.of(context) calls or manual color/border logic.

Project Structure #

  • doc/: Production-grade engineering specifications.
  • bin/: CLI entry point.
  • lib/core/: Global logic, infrastructure (network, configuration), and Design System.
  • lib/domain/: Business logic contracts and models.
  • lib/presentation/: UI screens and self-contained smart components.

Getting Started #

  1. Ensure FVM is installed.
  2. Run fvm flutter pub get.
  3. Generate boilerplate: fvm flutter pub run build_runner build --delete-conflicting-outputs.
  4. Run the app: fvm flutter run --dart-define=ENV=dev.
0
likes
150
points
657
downloads

Publisher

unverified uploader

Weekly Downloads

A robust Flutter bootstrapping tool and project template emphasizing a clean, layer-first architecture with strict state management.

Homepage
Repository (GitLab)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

args, connectivity_plus, cupertino_icons, dio, flutter, flutter_hooks, flutter_localizations, flutter_secure_storage, fpdart, freezed_annotation, google_fonts, hooks_riverpod, interact_cli, intl, json_annotation, logger, path, shared_preferences

More

Packages that depend on flutter_simple_architecture