flutter_simple_architecture 2.4.0 copy "flutter_simple_architecture: ^2.4.0" to clipboard
flutter_simple_architecture: ^2.4.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.
  • 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)
  4. test/presentation/profile/view_test.dart (Test 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.


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.
  • SideEffect Notifier: Non-interactive events (dialogs, alerts) are managed via a global sideEffectProvider. Notifiers emit SideEffect objects (Success, Failure, Info, Warning), and the root MyApp widget automatically displays the appropriate AppDialog.

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.

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/ using ThemeExtension.

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 #

  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.
0
likes
0
points
658
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

License

unknown (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