Skip to content

KalvadTech/flutter-mvvm-actions-arch

Repository files navigation

πŸš€ Flutter MVVM Actions Architecture

A Flutter application template using GetX for state management with MVVM + Actions Layer architecture. Includes pre-built modules for authentication, connectivity, localization, theme management, and a complete API integration pattern example.

✨ Key Features:

  • πŸ—οΈ Clean MVVM architecture with Actions layer for UX concerns
  • πŸ”„ Reactive state management with GetX
  • 🌐 Complete API integration pattern with apiFetch β†’ ApiResponse β†’ ApiHandler
  • πŸ” Authentication with token refresh and secure storage
  • πŸ“‘ Connectivity monitoring with offline handling
  • 🌍 Multi-language support with localization
  • 🎨 Light/Dark theme switching
  • πŸ“¦ Strategy-based HTTP response caching
  • πŸ“ Comprehensive documentation and coding guidelines

πŸ”Œ Built Like LEGO Blocks

This isn't your typical rigid template. It's built to adapt to your needs:

  • Plug & Play: Use the entire template as-is, or cherry-pick just the modules that fit your project
  • Layer Independence: Every architectural layer (Actions, ViewModels, Services) is decoupledβ€”swap any layer with your own implementation without breaking the rest
  • Framework Agnostic: Don't like GetX? No problem. Replace it with Riverpod, Bloc, or whatever you prefer. The architecture patterns remain solid regardless of your state management choice

Think of it as architecture-as-a-library rather than architecture-as-a-framework. You're in control.


πŸ“‘ Table of Contents


πŸ’­ Philosophy

Why MVVM + Actions Layer?

Real talkβ€”I was tired of choosing between messy MVVM code and over-engineered MVPVM setups that take forever to implement. I wanted that clean single responsibility vibe from MVPVM without the headache. So I built this template as the perfect middle ground: powerful enough for production apps, simple enough to actually enjoy using.

This template embraces separation of concerns at every level. Traditional MVVM often mixes UX concerns (loading overlays, error toasts, navigation) with business logic in ViewModels. By introducing an Actions layer, we achieve:

  • 🧼 Clean Controllers: ViewModels focus purely on business logic and state management
  • 🎯 Centralized UX: All loader overlays, error handling, and user feedback live in one place
  • πŸ” Consistency: Every user action follows the same pattern for error handling and feedback
  • πŸ§ͺ Testability: Each layer can be tested in isolation without mocking UI concerns
  • πŸ“– Readability: Code clearly expresses "what" (Actions) vs "how" (ViewModels)

Why GetX?

  • ⚑ Minimal Boilerplate: No need for BuildContext in most cases
  • πŸ”„ Reactive by Default: .obs and Obx() make state updates trivial
  • πŸ’‰ Smart DI: Lazy loading and automatic disposal of dependencies
  • 🧭 Simple Routing: Navigate without context, type-safe routes
  • 🌍 Built-in i18n: Translation system included

Clean Architecture Principles

  • Layers don't skip: View β†’ Action β†’ ViewModel β†’ Service β†’ Model (never View β†’ Service directly)
  • Dependencies point inward: Outer layers depend on inner layers, never the reverse
  • Immutable data: Models are immutable value objects with no business logic
  • Single Responsibility: Each class does one thing well
  • Fail loudly: Use typed exceptions (AppException, AuthException) not error codes

πŸ“ Conventions & Guidelines

Quick reference for architectural boundaries:

  • 🧼 Views: Pure Flutter widgets, zero business logic, delegate everything to Actions/ViewModels
  • πŸŽ›οΈ Actions: Handle UX concerns (loaders, toasts, error surfaces, navigation), wrap ViewModel calls, never fetch data directly
  • 🧠 Controllers: Orchestrate services, expose reactive state, contain business logic, manage lifecycles
  • 🌍 Services: Data layer only (HTTP, storage, external APIs), return domain models, no state management
  • πŸ“¦ Models: Immutable, JSON serialization only, no methods beyond fromJson/toJson, all fields final (use extensions for logic)
  • ⏲️ State Management: Prefer ApiResponse<T> over scattered boolean flags (isLoading, hasError)
  • πŸ”— Bindings: File names must match module names (posts_bindings.dart for posts/ module)
  • πŸ“ Naming: Use plural for collections (models/, posts/, connections/), singular for individual concepts

Golden Rule: If you're unsure where code belongs, ask: "Is this about what the user wants (Action), how to do it (ViewModel), or where to get it (Service)?"


πŸš€ Quick Start

Installation

You can use this template in two ways:

🎯 Option 1: Mason CLI (Recommended)

mason logo

Pub mason License: MIT

1. Install Mason CLI:

dart pub global activate mason_cli

2. Add the template:

mason add -g --source git https://github.com/KalvadTech/flutter-mvvm-actions-arch --path lib/mason/bricks/flutter_mvvm_actions

3. Verify installation:

mason list
# Output: flutter_mvvm_actions - A Flutter application template using GetX...

4. Create a new Flutter project:

flutter create my_app
cd my_app/lib

5. Generate from template:

mason make flutter_mvvm_actions

You'll be prompted for:

  • project_name - Your project name
  • description - Brief description
  • org_name - Organization identifier (e.g., com.example)

πŸ“‹ Option 2: Manual

  1. Clone this repository
  2. Copy the lib/src/ directory to your Flutter project's lib/ folder
  3. Copy desired modules (or all of them)
  4. Update pubspec.yaml with required dependencies (see below)

⚠️ Note: If copying only specific modules, ensure you also copy:

  • lib/src/core/ - Core framework (bindings, routing, errors, presentation)
  • lib/src/infrastructure/ - HTTP, storage, cache services
  • lib/src/config/ - API configuration
  • lib/src/utils/ - Utilities and helpers

πŸ“¦ Required Packages

Add these to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter

  # State Management & DI
  get: ^4.6.6

  # UI Components
  loader_overlay: ^4.0.3
  flutter_spinkit: ^5.2.1
  flutter_svg: ^2.0.13
  google_fonts: ^6.2.1

  # Connectivity
  connectivity_plus: ^6.1.0

  # Storage
  get_storage: ^2.1.1
  flutter_secure_storage: ^9.2.2
  path_provider: ^2.1.5

  # Utilities
  intl: ^0.19.0
  jwt_decoder: ^2.0.1
  sentry: ^8.9.0
  timeago: ^3.7.0

▢️ Run

flutter pub get
flutter run

πŸ—οΈ Architecture

This template uses MVVM (Model-View-ViewModel) augmented with an Actions layer for user intent handling:

User Interaction
    ↓
🧼 View (Flutter Widgets)
    ↓
πŸŽ›οΈ Actions (ActionPresenter) ← Loader, Error Handling, Feedback, Navigation
    ↓
🧠 ViewModel (GetxController) ← Business Logic, State Management
    ↓
🌍 Service (ApiService) ← HTTP, Storage, Data Access
    ↓
πŸ“¦ Model (Data Classes) ← JSON Serialization

πŸ”‘ Core Components

ActionPresenter

Base class for user intent handlers that wraps ViewModel calls with UX concerns:

  • βœ… Shows global loader overlay during async operations
  • βœ… Catches AppException and shows user-friendly error messages
  • βœ… Reports exceptions to Sentry for crash analytics
  • βœ… Safely guards overlay show/hide to prevent crashes in tests

Usage:

class AuthActions extends ActionPresenter {
  Future signIn(BuildContext context) async {
    actionHandler(context, () async {
      await _authViewModel.signIn();
      showSuccessSnackBar('Success', 'Signed in successfully');
    });
  }
}

ApiResponse Pattern

Type-safe state container for async operations:

enum ApiStatus { idle, loading, success, error }

class ApiResponse<T> {
  final T? data;
  final Object? error;
  final ApiStatus status;

  bool get isLoading => status == ApiStatus.loading;
  bool get isSuccess => status == ApiStatus.success && data != null;
  bool get hasError => status == ApiStatus.error;
}

apiFetch() Helper: Wraps service calls and emits Stream<ApiResponse<T>>:

apiFetch(_service.fetchData).listen((state) => _data.value = state);
// Emits: loading β†’ success(data) OR error(message)

ApiHandler Widget

Smart widget that automatically renders UI based on ApiResponse state:

ApiHandler<List<PostModel>>(
  response: controller.posts,          // ApiResponse state
  tryAgain: controller.refreshData,    // Retry callback
  isEmpty: (posts) => posts.isEmpty,   // Empty check
  successBuilder: (posts) => ListView(...), // Success UI

  // Optional customizations:
  loadingWidget: CustomLoader(),
  errorBuilder: (msg) => CustomError(msg),
  emptyBuilder: () => EmptyState(),
)

States handled automatically:

  • ⏳ Loading - Shows spinner or custom loading widget
  • ❌ Error - Shows error message with retry button
  • πŸ“­ Empty - Shows empty state message
  • βœ… Success - Builds UI with data using successBuilder

ApiService

Centralized HTTP client extending GetConnect:

Features:

  • βœ… Automatic Authorization: Bearer <token> header injection
  • βœ… Token refresh on 401 with one-shot retry
  • βœ… Typed exception mapping (AuthException, APIException)
  • βœ… Redacted logging (masks sensitive headers)
  • βœ… 120s timeout with configurable duration
  • βœ… Optional response caching with strategy pattern
  • βœ… File download support with progress callbacks

Configuration: All API URLs must be defined in lib/src/config/api_config.dart:

class APIConfiguration {
  static const bool isDevelopment = true;
  static const String baseUrl = isDevelopment ? _stagingUrl : _productionUrl;

  static const String signInUrl = '$baseUrl/signin';
  static const String postsUrl = '$baseUrl/posts';
}

πŸ“ Template Structure

Overall Organization

lib/src/
β”œβ”€β”€ app.dart                      # 🎯 Main app widget
β”œβ”€β”€ main.dart                     # πŸš€ Entry point
β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ api_config.dart          # 🌐 API endpoints
β”‚   β”œβ”€β”€ assets.dart              # πŸ–ΌοΈ Asset paths
β”‚   β”œβ”€β”€ colors.dart              # 🎨 Color constants
β”‚   β”œβ”€β”€ styles.dart              # πŸ“ Text styles
β”‚   β”œβ”€β”€ themes.dart              # 🎨 Theme definitions
β”‚   └── config.dart              # βš™οΈ Config barrel export
β”œβ”€β”€ core/                        # πŸ›οΈ Framework-level code
β”‚   β”œβ”€β”€ errors/                  # ❌ Exception classes
β”‚   β”œβ”€β”€ presentation/            # 🎨 ApiResponse, ActionPresenter
β”‚   β”œβ”€β”€ bindings/
β”‚   β”‚   └── bindings.dart        # πŸ’‰ Global DI
β”‚   └── routing/
β”‚       └── route_manager.dart   # 🧭 Navigation & routing
β”œβ”€β”€ infrastructure/              # πŸ”§ Concrete adapters
β”‚   β”œβ”€β”€ http/                    # 🌐 ApiService
β”‚   β”œβ”€β”€ cache/                   # πŸ’Ύ Caching system
β”‚   └── storage/                 # πŸ’Ώ AppStorageService
β”œβ”€β”€ presentation/                # 🎨 Shared UI components
β”‚   β”œβ”€β”€ custom/                  # 🧱 Custom reusable widgets
β”‚   β”‚   β”œβ”€β”€ custom_button.dart
β”‚   β”‚   β”œβ”€β”€ custom_card.dart
β”‚   β”‚   β”œβ”€β”€ custom_form_field.dart
β”‚   β”‚   β”œβ”€β”€ custom_text.dart
β”‚   β”‚   └── customs.dart         # Barrel export
β”‚   └── widgets/                 # πŸ”§ Core widgets
β”‚       └── api_handler.dart     # ApiResponse UI handler
β”œβ”€β”€ utils/                       # πŸ› οΈ Utilities and helpers
β”‚   β”œβ”€β”€ date_time_extensions.dart # πŸ“… DateTime/Duration extensions
β”‚   β”œβ”€β”€ form_validators.dart     # βœ… Form input validators
β”‚   β”œβ”€β”€ responsive_utils.dart    # πŸ“± Responsive design helpers
β”‚   └── themes.dart              # 🎨 Theme configurations
└── modules/                     # πŸ“¦ Feature modules
    β”œβ”€β”€ auth/                    # πŸ” Authentication
    β”œβ”€β”€ connections/             # πŸ“‘ Connectivity
    β”œβ”€β”€ locale/                  # 🌍 Localization
    β”œβ”€β”€ theme/                   # 🎨 Theme switching
    β”œβ”€β”€ posts/                   # πŸ“ API example
    └── menu/                    # πŸ” Navigation menu

Feature Module Structure

Each feature module follows a consistent structure:

lib/src/modules/<feature>/
β”œβ”€β”€ actions/                      # πŸŽ›οΈ User intent handlers (optional)
β”‚   └── <feature>_actions.dart   # Extends ActionPresenter
β”œβ”€β”€ controllers/                  # 🧠 ViewModels
β”‚   └── <feature>_view_model.dart # Extends GetxController
β”œβ”€β”€ data/
β”‚   β”œβ”€β”€ models/                  # πŸ“¦ Domain objects
β”‚   β”‚   └── <feature>_model.dart # fromJson/toJson serialization
β”‚   └── services/                # 🌍 Data access layer
β”‚       └── <feature>_service.dart # Extends ApiService
β”œβ”€β”€ views/                       # 🧼 UI layer
β”‚   β”œβ”€β”€ <feature>_page.dart     # Main page
β”‚   └── widgets/                 # Feature-specific widgets
β”œβ”€β”€ <feature>_bindings.dart      # πŸ’‰ GetX dependency injection
└── <feature>.dart               # πŸ“¦ Barrel export file

Layer Responsibilities

πŸŽ›οΈ actions/ - User Intent Handlers

Extends ActionPresenter base class to handle user interactions with:

  • Global loader overlay - Shows/hides loading spinner during async operations
  • Unified error handling - Catches exceptions and displays user-friendly messages
  • Success feedback - Shows snackbars/toasts for successful operations
  • Sentry reporting - Automatically reports errors to crash analytics

Example:

class AuthActions extends ActionPresenter {
  Future signIn(BuildContext context) async {
    actionHandler(context, () async {
      await _authViewModel.signIn();
      showSuccessSnackBar('Success', 'Signed in successfully');
    });
  }
}

🧠 controllers/ - ViewModels

Extends GetxController for business logic and state management:

  • Reactive state with Rx<T> observables
  • Business logic orchestration - Coordinates service calls
  • State wrapping - Uses ApiResponse<T> for loading/success/error states
  • Lifecycle management - onInit(), onClose() hooks

Example:

class PostViewModel extends GetxController {
  final Rx<ApiResponse<List<PostModel>>> _posts = ApiResponse.idle().obs;

  ApiResponse<List<PostModel>> get posts => _posts.value;

  @override
  void onInit() {
    super.onInit();
    _fetchPosts();
  }

  void _fetchPosts() {
    apiFetch(_postService.fetchPosts).listen((value) => _posts.value = value);
  }
}

πŸ“¦ data/models/ - Domain Objects

Plain Dart classes representing domain entities:

  • Immutable - All fields are final
  • JSON serialization - fromJson factory and toJson method
  • No business logic - Pure data containers

Example:

class PostModel {
  final int id;
  final String title;

  PostModel({required this.id, required this.title});

  factory PostModel.fromJson(Map<String, dynamic> json) => PostModel(
    id: json["id"],
    title: json["title"],
  );

  Map<String, dynamic> toJson() => {"id": id, "title": title};
}

🌍 data/services/ - Data Access Layer

Extends ApiService for HTTP communication:

  • Automatic auth headers - Injects bearer tokens
  • Retry on 401 - Refreshes token and retries once
  • Error mapping - Converts HTTP errors to typed exceptions
  • Redacted logging - Logs requests with sensitive data masked
  • Optional caching - Strategy-based response caching

Example:

class PostService extends ApiService {
  Future<List<PostModel>> fetchPosts() async {
    final response = await get(APIConfiguration.postsUrl);
    return postModelFromJson(response.body);
  }
}

🧼 views/ - Presentation Layer

Pure Flutter widgets:

  • Observes ViewModel via GetX<ViewModel> or Obx
  • No business logic - Delegates to Actions/ViewModels
  • No direct service calls - Uses ViewModels only
  • Form validation - Handles UI-level validation

🧩 Modules

All pre-built modules are located in the lib/src/modules/ directory.

πŸ” Authentication

Complete authentication flow with login/register pages, form validation, and token management.

Architecture:

  • AuthActions - Extends ActionPresenter for sign-in/sign-up/logout with loader overlay and error handling
  • AuthViewModel - Manages reactive auth state (checking β†’ authenticated | notAuthenticated)
  • AuthService - Extends ApiService for HTTP calls, validates response shape, persists tokens via AppStorageService
  • UserModel - User data model with JSON serialization

Features:

  • βœ… Username/password authentication with validation
  • βœ… Token refresh on 401 with automatic retry
  • βœ… Secure token storage (Keychain/Keystore)
  • βœ… Session checking on app start
  • βœ… Custom login/register pages with form validation
  • βœ… Auth state machine with definitive states

Configuration:

  1. Update API URLs in lib/src/config/api_config.dart
  2. Update UserModel in lib/src/modules/auth/data/models/user.dart to match your API response
  3. Customize login.dart and register.dart pages if needed

Auth Flow:

  • App start: AuthViewModel.checkSession() verifies tokens β†’ authenticated or notAuthenticated
  • Login: View validates β†’ AuthActions.signIn() shows loader β†’ AuthViewModel.signIn() calls service β†’ tokens saved β†’ state updated
  • Logout: AuthViewModel.logout() clears tokens β†’ notAuthenticated state

πŸ“– Documentation: See docs/architecture/auth_module.md for detailed architecture and testing guide.

πŸ“‘ Connections

Real-time connectivity monitoring with offline/online detection and reachability probing.

Architecture:

  • ConnectionViewModel - Reactive connectivity state (wifi/mobile/connecting/offline/noInternet)
  • ConnectionOverlay - Global status bar showing connectivity changes
  • ConnectionHandler - Widget that conditionally renders content based on connection state
  • ReachabilityService - DNS/HTTP probing to verify internet access

Features:

  • βœ… Wi-Fi, mobile data, and offline detection
  • βœ… Reachability probing (DNS to google.com by default)
  • βœ… Debounced connectivity events (250ms) to reduce flapping
  • βœ… Automatic connectivity re-check on app resume
  • βœ… Offline duration timer for telemetry
  • βœ… Material 3 themed overlays with SafeArea and Semantics

Usage:

Global Overlay (shows connectivity status app-wide):

ConnectionOverlay(
  child: GetMaterialApp(...),
)

Conditional Rendering (show content only when connected):

ConnectionHandler(
  connectedWidget: MyContent(),
  onConnectingWidget: ConnectingIndicator(),
  onRetry: () => controller.checkConnection(),
)

State Management:

  • ConnectivityType.wifi and ConnectivityType.mobileData = connected
  • ConnectivityType.connecting = probing reachability (NOT considered connected)
  • ConnectivityType.noInternet = transport exists but no internet access

πŸ“– Documentation: See docs/architecture/connectivity_module.md for configuration, testing, and telemetry callbacks.

πŸ“ Posts Example

Reference implementation demonstrating the complete API integration pattern using JSONPlaceholder public API.

Purpose:

  • πŸ“š Educational example of apiFetch β†’ ApiResponse β†’ ApiHandler pattern
  • 🎯 Shows best practices for API integration
  • πŸ”„ Demonstrates pull-to-refresh, error handling, and empty states
  • πŸ“ Serves as template for creating your own API modules

What It Demonstrates:

  • PostModel with fromJson/toJson serialization
  • PostService extending ApiService with GET requests
  • PostViewModel using apiFetch() pattern and onInit() initialization
  • PostsPage with ApiHandler and RefreshIndicator
  • PostsBindings with permanent ViewModel registration
  • Material Design card-based UI with user badges

Features:

  • βœ… Fetches 100 blog posts from https://jsonplaceholder.typicode.com/posts
  • βœ… Automatic loading spinner
  • βœ… Error handling with retry button
  • βœ… Empty state detection
  • βœ… Pull-to-refresh support
  • βœ… Persistent state across navigation

How to Adapt for Your API:

  1. Replace PostModel with your data model
  2. Update PostService to call your API endpoints
  3. Configure URLs in APIConfiguration
  4. Customize PostListTile widget for your design
  5. Add POST/PUT/DELETE methods as needed

πŸ“– Complete Guide: See docs/examples/api_pattern_example.md

🌍 Localization

Multi-language support with dynamic language switching and persistent preferences.

Architecture:

  • LocalizationViewModel - Manages selected language and locale state
  • LocalizationService - Loads translations and handles GetX locale updates
  • LanguageModel - Language metadata (code, name, flag)
  • LanguagePickerDialog - UI for language selection

Features:

  • βœ… Arabic and English built-in (easily add more languages)
  • βœ… Translation key management in config/keys.dart
  • βœ… Right-to-left (RTL) support
  • βœ… Persistent language preference
  • βœ… Date formatting with locale awareness

How to Add a Language:

  1. Add language to LocalizationViewModel.supportedLanguages
  2. Create translation file in lib/src/modules/locale/data/lang/<language_code>.dart
  3. Add translation keys with values

Usage:

Text('auth.login.title'.tr) // Uses GetX translation

🎨 Theme

Light and dark theme switching with Material 3 design and persistent preferences.

Architecture:

  • ThemeViewModel - Manages theme mode state (light/dark/system)
  • ThemeService - Handles theme persistence
  • ThemeSwitch - Switch widget for theme toggling
  • ThemeDrawerItem - Drawer list tile for theme selection

Features:

  • βœ… Material 3 design system
  • βœ… Rosy-red seed color (Color(0xFFE91E63))
  • βœ… System theme detection
  • βœ… Persistent theme preference
  • βœ… Google Fonts integration

Color Scheme: Uses Material 3 ColorScheme.fromSeed() with rosy-red seed color for automatic color generation.

Usage:

ThemeSwitch() // Switch widget
ThemeDrawerItem() // Drawer list tile

πŸ” Menu

Simple drawer menu with navigation to all modules and theme/language pickers.

Features:

  • βœ… Navigation to all module pages
  • βœ… Theme switcher integration
  • βœ… Language picker integration
  • βœ… Material 3 design

πŸ”Œ API Integration Pattern

This template provides a complete, type-safe API integration pattern:

Flow: Service β†’ apiFetch() β†’ ApiResponse<T> β†’ ApiHandler<T> β†’ UI

🎯 Quick Example

1️⃣ Model:

class PostModel {
  final int id;
  final String title;

  factory PostModel.fromJson(Map<String, dynamic> json) => PostModel(
    id: json["id"],
    title: json["title"],
  );
}

2️⃣ Service:

class PostService extends ApiService {
  Future<List<PostModel>> fetchPosts() async {
    final response = await get(APIConfiguration.postsUrl);
    return postModelFromJson(response.body);
  }
}

3️⃣ ViewModel:

class PostViewModel extends GetxController {
  final Rx<ApiResponse<List<PostModel>>> _posts = ApiResponse.idle().obs;

  ApiResponse<List<PostModel>> get posts => _posts.value;

  @override
  void onInit() {
    super.onInit();
    _fetchPosts();
  }

  void _fetchPosts() {
    apiFetch(_postService.fetchPosts).listen((value) => _posts.value = value);
  }

  void refreshData() => _fetchPosts();
}

4️⃣ View:

class PostsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GetX<PostViewModel>(
        builder: (controller) => RefreshIndicator(
          onRefresh: () async => controller.refreshData(),
          child: ApiHandler<List<PostModel>>(
            response: controller.posts,
            tryAgain: controller.refreshData,
            isEmpty: (posts) => posts.isEmpty,
            successBuilder: (posts) => ListView.builder(
              itemCount: posts.length,
              itemBuilder: (context, index) => ListTile(
                title: Text(posts[index].title),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

5️⃣ Bindings:

class PostsBindings implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<PostService>(() => PostService(), fenix: true);
    Get.put<PostViewModel>(PostViewModel(Get.find()), permanent: true);
  }
}

πŸ“– Complete Guide: See docs/examples/api_pattern_example.md for detailed documentation with advanced patterns, POST requests, and migration guide.


βš™οΈ Infrastructure Overview

This template splits cross-cutting concerns into clear layers:

πŸ›οΈ Core Layer (lib/src/core/)

Primitives and contracts:

  • core/errors/ - AppException, AuthException, APIException
  • core/presentation/ - ApiResponse, ActionPresenter

πŸ”§ Infrastructure Layer (lib/src/infrastructure/)

Concrete adapters:

🌐 HTTP (infrastructure/http/)

  • ApiService - Centralizes headers, retry-after-refresh, error mapping, logging, and optional caching

πŸ’Ύ Cache (infrastructure/cache/)

Strategy-based HTTP response caching:

  • CacheStore - Storage interface
  • CacheKeyStrategy - Cache key generation
  • CachePolicy - Read/write rules and TTL
  • CacheManager - Orchestrator
  • Implementations: GetStorageCacheStorage, DefaultCacheKeyStrategy, SimpleTimeToLiveCachePolicy

Enable caching in main():

final store = await GetStorageCacheStorage.create(
  container: AppStorageService.container,
);
final cache = CacheManager(
  store,
  const DefaultCacheKeyStrategy(),
  SimpleTimeToLiveCachePolicy(timeToLive: Duration(hours: 6)),
);
ApiService.configureCache(cache);

Disable globally:

ApiService.configureCache(null);

Caching Rules:

  • GET requests use cache by default
  • POST/PUT/DELETE do not cache unless explicitly opted in
  • Only 2xx responses are cached
  • Cache stores raw response text; decoding happens in ApiService

πŸ’Ώ Storage (infrastructure/storage/)

  • AppStorageService - Unified facade for device-local storage
  • PreferencesStorage - GetStorage backend for preferences
  • SecureTokenStorage - Keychain/Keystore backend for sensitive tokens

Initialize in main():

await AppStorageService.instance.initialize();

Usage:

// Read token
final token = AppStorageService.instance.accessToken;

// Write token
await AppStorageService.instance.setAccessToken('token');

// Preferences
await AppStorageService.instance.preferences.write('key', 'value');
final value = AppStorageService.instance.preferences.read('key');

⚠️ Deprecation Note: MemoryService is deprecated. Use AppStorageService for all storage needs.

πŸ“₯ File Downloads

ApiService.downloadFile() reuses GetConnect.get with ResponseType.bytes for uniform behavior:

  • βœ… Includes auth headers
  • βœ… Retry-after-refresh on 401
  • βœ… Honors timeout and logging
  • βœ… Optional progress callback (reports completion)
  • βœ… Writes bytes to disk

Note: For streaming progress or proxy/certificate customization, implement a specialized downloader.


πŸ› οΈ Utilities & Helpers

This template provides a comprehensive set of utilities to simplify common tasks, located in /src/utils/ and /src/core/.

🧭 RouteManager - Centralized Navigation

Location: lib/src/core/routing/route_manager.dart

Centralized navigation management wrapping GetX routing. Always use RouteManager instead of direct Get.* calls for consistency across the app.

Navigation Helpers:

// Specific routes
RouteManager.toAuth();           // Navigate to login
RouteManager.toRegister();       // Navigate to registration
RouteManager.toMenu();           // Navigate to menu

// Auth flow helpers
RouteManager.logout();           // Clear stack, go to auth
RouteManager.loginSuccess();     // Clear stack, go to menu

// Generic navigation
RouteManager.to('/custom-route', arguments: data);
RouteManager.off('/custom-route');      // Replace current
RouteManager.offAll('/custom-route');   // Clear stack
RouteManager.back(result: data);        // Go back

// Utilities
String current = RouteManager.currentRoute;
bool isOnAuth = RouteManager.isCurrentRoute(RouteManager.authRoute);
dynamic args = RouteManager.arguments;

Benefits:

  • Centralized navigation logic
  • Type-safe route constants
  • Consistent patterns across app
  • Easier testing and debugging

βœ… FormValidators - Input Validation

Location: lib/src/utils/form_validators.dart

Static validator methods for form inputs with built-in i18n error messages.

Usage:

TextFormField(
  validator: FormValidators.email,      // Email validation
)

TextFormField(
  validator: FormValidators.phone,      // Phone validation
)

TextFormField(
  validator: FormValidators.password,   // Password strength
)

// Combine multiple validators
TextFormField(
  validator: FormValidators.combine([
    FormValidators.required,
    FormValidators.email,
  ]),
)

// Custom validation
TextFormField(
  validator: FormValidators.custom(
    regex: RegExp(r'^[A-Z]{2}\d{4}$'),
    errorMessage: 'Invalid format (e.g., AB1234)',
  ),
)

Configurable Regex:

// Override default patterns globally
FormValidators.emailRegex = RegExp(r'your-custom-pattern');
FormValidators.phoneRegex = RegExp(r'your-phone-pattern');

πŸ“± ResponsiveUtils - Responsive Design

Location: lib/src/utils/responsive_utils.dart

Complete responsive design system with breakpoint-based layouts.

Device Type Detection:

if (ResponsiveUtils.isMobile(context)) {
  // Mobile: width < 600
}

if (ResponsiveUtils.isTablet(context)) {
  // Tablet: 600 <= width < 1024
}

if (ResponsiveUtils.isDesktop(context)) {
  // Desktop: 1024 <= width < 1440
}

if (ResponsiveUtils.isWideDesktop(context)) {
  // Wide Desktop: width >= 1440
}

Responsive Values:

// Type-safe responsive values
double padding = ResponsiveUtils.valueByDevice<double>(
  context,
  mobile: 16,
  tablet: 24,
  desktop: 32,
  wide: 40,
);

// Scaled font sizes
double fontSize = ResponsiveUtils.scaledFontSize(context, 16);
// mobile: 16, tablet: 17.6, desktop: 18.4, wide: 19.2

// Responsive padding/margin
double padding = ResponsiveUtils.padding(context);

// Grid columns
int columns = ResponsiveUtils.gridColumns(context);
// mobile: 1, tablet: 2, desktop: 3, wide: 4

Custom Breakpoints:

// Override defaults globally
ResponsiveUtils.mobileBreakpoint = 640;
ResponsiveUtils.tabletBreakpoint = 1024;
ResponsiveUtils.desktopBreakpoint = 1536;

πŸ“… DateTime Extensions - Fluent Date API

Location: lib/src/utils/date_time_extensions.dart

Fluent API for DateTime and Duration operations using Dart extensions.

Date Comparisons:

final date = DateTime.now();

if (date.isToday) {
  // Same calendar day as today
}

if (date.isYesterday) {
  // Calendar day before today
}

if (date.isSameDay(otherDate)) {
  // Same calendar day
}

Date Manipulation:

final start = date.startOfDay;    // 00:00:00
final end = date.endOfDay;        // 23:59:59
int days = date.daysBetween(otherDate);

Formatting:

String formatted = date.format();                // 'dd/MM/yyyy'
String formatted = date.format('yyyy-MM-dd');    // Custom pattern

String time = date.formatTime();                 // 'HH:mm'
String datetime = date.formatDateTime();         // 'dd/MM/yyyy HH:mm'

String relative = date.timeAgo();
// "just now", "5 minutes ago", "2 hours ago", "yesterday",
// "3 days ago", "in 5 minutes", "in 2 days"

Duration Extensions:

final duration = Duration(hours: 2, minutes: 5, seconds: 30);
String formatted = duration.format();  // "02:05:30"

Why Extensions? More idiomatic than static utility methods (date.isToday vs DateTimeUtils.isToday(date)), fluent and discoverable API.

🧩 Core vs Utils

/src/core/ - Framework-level code:

  • Bindings (dependency injection)
  • Routing (navigation management)
  • Errors (exception classes)
  • Presentation (ApiResponse, ActionPresenter)

/src/utils/ - Helper functions and extensions:

  • Form validators
  • Responsive utilities
  • DateTime extensions
  • Theme configurations

This separation provides clear distinction between framework concerns and helpers, making the codebase more organized and maintainable.


❓ FAQ

❓ Why Actions instead of putting UX in the ViewModel?

Short answer: To centralize UX plumbing and keep controllers focused on logic.

Long answer: Traditional MVVM often results in ViewModels that manage both business logic AND UX concerns (loading flags, error messages, navigation). This violates Single Responsibility Principle. By extracting UX concerns into Actions:

  • ViewModels stay pure business logic
  • All user interactions get consistent error handling and feedback
  • Testing becomes easier (no need to mock loaders or verify toast calls)
  • You can swap out UX strategy (e.g., from overlay to inline loaders) in one place

πŸ”„ Can I swap GetX for another state management solution later?

Yes β€” most pieces are framework-agnostic; you'll just need to replace DI/observable wrappers.

What's coupled to GetX:

  • GetxController base class in ViewModels β†’ Replace with your state solution (Bloc, Riverpod, etc.)
  • Get.find() dependency injection β†’ Replace with Provider, get_it, etc.
  • .obs and Obx() reactivity β†’ Replace with your reactive primitives
  • Get.to() navigation β†’ Use Navigator or your preferred router

What's NOT coupled to GetX:

  • Models (pure Dart classes)
  • Services (extend GetConnect, but can be rewritten to use Dio/http)
  • Actions pattern (just needs BuildContext and error handling)
  • ApiResponse pattern (pure state container)

Migration effort: Moderate. Budget 2-4 hours for a small app, 1-2 days for large apps.

πŸ“Š Where do I add analytics/crash reporting?

Hook into ActionPresenter and ApiService interceptors:

For Analytics (user actions): Override ActionPresenter.actionHandler():

class MyActionPresenter extends ActionPresenter {
  @override
  Future actionHandler(BuildContext context, Future Function() action) async {
    final stopwatch = Stopwatch()..start();
    try {
      await super.actionHandler(context, action);
      analytics.logEvent('action_success', {'duration': stopwatch.elapsedMilliseconds});
    } catch (e) {
      analytics.logEvent('action_error', {'error': e.toString()});
      rethrow;
    }
  }
}

For Crash Reporting (API errors): ActionPresenter already reports to Sentry in the reportError() method. To customize:

@override
void reportError(Object error, StackTrace stackTrace) {
  Sentry.captureException(error, stackTrace: stackTrace);
  FirebaseCrashlytics.instance.recordError(error, stackTrace);
}

For API Logging: ApiService already logs all requests. To add telemetry:

class MyApiService extends ApiService {
  @override
  Future<Response<T>> get<T>(String url, ...) async {
    analytics.logEvent('api_request', {'endpoint': url, 'method': 'GET'});
    return super.get(url, ...);
  }
}

πŸ§ͺ How do I test Actions and ViewModels?

ViewModels are easy - they're pure logic:

test('fetchPosts emits loading then success', () async {
  final service = MockPostService();
  final viewModel = PostViewModel(service);

  when(service.fetchPosts()).thenAnswer((_) async => [PostModel(id: 1)]);

  viewModel.onInit();

  expect(viewModel.posts.isLoading, true);
  await Future.delayed(Duration.zero); // Wait for async
  expect(viewModel.posts.isSuccess, true);
  expect(viewModel.posts.data?.length, 1);
});

Actions require context - use widget tests:

testWidgets('signIn shows loader and navigates on success', (tester) async {
  final viewModel = MockAuthViewModel();
  Get.put(viewModel);

  await tester.pumpWidget(MaterialApp(home: LoginPage()));
  await tester.tap(find.text('Sign In'));
  await tester.pump();

  expect(find.byType(LoaderOverlay), findsOneWidget);
  verify(viewModel.signIn()).called(1);
});

πŸ” How does token refresh work?

Automatic and transparent:

  1. ApiService detects 401 Unauthorized response
  2. Calls ApiService.refreshSession() (base class implementation) with refresh token
  3. Saves new access token to AppStorageService
  4. Retries original request with new token (one time only)
  5. If refresh fails, throws AuthException β†’ user logged out

Key Points:

  • Token refresh is handled automatically by ApiService base class
  • AuthService does not override this method
  • All HTTP requests benefit from automatic token refresh on 401
  • Tokens are stored in AppStorageService (secure storage for tokens, GetStorage for preferences)

No token refresh endpoint? Override ApiService._retryAfterRefresh() to return false.

πŸ’Ύ When should I use caching?

Use caching for:

  • βœ… Reference data that changes infrequently (categories, settings, config)
  • βœ… Large lists that are expensive to fetch (product catalogs)
  • βœ… Offline-first features (reading cached data when offline)

Don't use caching for:

  • ❌ Real-time data (live scores, stock prices, chat messages)
  • ❌ User-specific data that changes often (cart, notifications)
  • ❌ POST/PUT/DELETE requests (mutations shouldn't be cached)

Default behavior:

  • GET requests are cached (6-hour TTL by default)
  • All other methods bypass cache
  • Cache can be disabled globally: ApiService.configureCache(null)

🌐 How do I add a new API endpoint?

4 steps:

1️⃣ Add URL to config:

// lib/src/config/api_config.dart
static const String usersUrl = '$baseUrl/users';

2️⃣ Create Model:

// lib/src/modules/users/data/models/user_model.dart
class UserModel {
  final int id;
  final String name;

  factory UserModel.fromJson(Map<String, dynamic> json) => ...
}

3️⃣ Create Service:

// lib/src/modules/users/data/services/user_service.dart
class UserService extends ApiService {
  Future<List<UserModel>> fetchUsers() async {
    final response = await get(APIConfiguration.usersUrl);
    return userModelFromJson(response.body);
  }
}

4️⃣ Use in ViewModel:

apiFetch(_userService.fetchUsers).listen((state) => _users.value = state);

πŸ“¦ Can I use only specific modules?

Yes! Modules are designed to be independent.

To use only Auth + Theme:

  1. Copy lib/src/core/ (required for all modules)
  2. Copy lib/src/infrastructure/ (required for API/storage)
  3. Copy lib/src/config/ (API configuration)
  4. Copy lib/src/utils/ (utilities and helpers)
  5. Copy lib/src/modules/auth/ and lib/src/modules/theme/
  6. Remove unused bindings from lib/src/core/bindings/bindings.dart
  7. Remove unused routes from lib/src/core/routing/route_manager.dart

Dependency note: Auth module requires AppStorageService from infrastructure.

🎯 What's the difference between actions/ and controllers/?

Quick rule: Actions = WHAT (user wants), Controllers = HOW (to do it)

Actions (User Intent):

  • Triggered by user interactions (button press, form submit)
  • Shows loaders, handles errors, shows feedback
  • Calls ViewModel methods
  • Lives in actions/ folder
  • Extends ActionPresenter

Controllers (Business Logic):

  • Orchestrates services, manages state
  • Contains business rules and validation
  • Exposes reactive state to views
  • Lives in controllers/ folder
  • Extends GetxController

Example:

// ACTION: User wants to sign in
AuthActions.signIn(context) {
  actionHandler(() async {
    await _viewModel.signIn(); // Calls controller
    showSuccessSnackBar('Welcome!');
  });
}

// CONTROLLER: How to sign in
AuthViewModel.signIn() async {
  final user = await _authService.signIn(email, password); // Calls service
  _user.value = user; // Updates state
}

πŸ”Œ How do I handle WebSocket/GraphQL?

WebSocket: Create a WebSocketService in infrastructure/:

class WebSocketService {
  late IOWebSocketChannel _channel;

  Stream<dynamic> connect(String url) {
    _channel = IOWebSocketChannel.connect(url);
    return _channel.stream;
  }

  void send(dynamic message) => _channel.sink.add(message);
  void close() => _channel.sink.close();
}

Use in ViewModel:

class ChatViewModel extends GetxController {
  final _messages = <Message>[].obs;

  @override
  void onInit() {
    _webSocket.connect(chatUrl).listen((data) {
      _messages.add(Message.fromJson(data));
    });
  }
}

GraphQL: Replace ApiService base class with GraphQL client (e.g., graphql_flutter):

class GraphQLService {
  final GraphQLClient client;

  Future<T> query<T>(String query, {Map<String, dynamic>? variables}) async {
    final result = await client.query(QueryOptions(
      document: gql(query),
      variables: variables ?? {},
    ));
    if (result.hasException) throw APIException(result.exception.toString());
    return result.data as T;
  }
}

Pattern stays the same: Service β†’ ViewModel β†’ View.

🎯 Where do I put model logic/computed properties?

Short answer: Use extensions, not methods in the model class.

Why: Models should be pure data containers for JSON serialization. Adding logic violates single responsibility and makes serialization complex.

Example:

// ❌ Bad - Logic in model
class UserModel {
  final String firstName;
  final String lastName;

  String get fullName => '$firstName $lastName'; // Don't do this

  factory UserModel.fromJson(Map<String, dynamic> json) => ...
}

// βœ… Good - Pure model + Extension
class UserModel {
  final String firstName;
  final String lastName;

  factory UserModel.fromJson(Map<String, dynamic> json) => ...
  Map<String, dynamic> toJson() => ...
}

extension UserModelExtensions on UserModel {
  String get fullName => '$firstName $lastName';
  bool get isValid => firstName.isNotEmpty && lastName.isNotEmpty;
  String initials() => '${firstName[0]}${lastName[0]}'.toUpperCase();
}

Usage:

final user = UserModel.fromJson(json);
Text(user.fullName); // Extension method works seamlessly

Benefits:

  • βœ… Models stay serializable and focused
  • βœ… Extensions are discoverable (autocomplete works)
  • βœ… Can add multiple extension files for different concerns
  • βœ… Follows single responsibility principle

πŸ“š Documentation

Comprehensive documentation is available in the docs/ directory:

πŸ›οΈ Architecture

  • docs/architecture/coding_guidelines.md - Naming conventions, MVVM pattern, documentation style, module structure, utilities documentation
  • docs/architecture/auth_module.md - Authentication flow, state machine, dependency management with callbacks, testing guide
  • docs/architecture/connectivity_module.md - Connectivity states, reachability, configuration

πŸ“– Examples

  • docs/examples/api_pattern_example.md - Complete API integration guide with step-by-step implementation, architecture flow diagram, POST request examples, and migration guide

πŸ”„ Migration

  • MIGRATION_GUIDE.md - Complete migration guide for v0.3.0+ with breaking changes, file structure updates, API changes, step-by-step instructions, and troubleshooting

πŸ“ Changelog

  • CHANGELOG.md - Full log of changes, migrations, and deprecations

πŸ†• What's New in v0.3.0?

Major refactoring focused on better organization and cleaner APIs:

  • Core directory created - Framework code moved to /src/core/ (bindings, routing)
  • DateTime extensions - Fluent API replacing static utilities (date.isToday vs DateTimeUtils.isToday(date))
  • Enhanced ResponsiveUtils - Complete responsive system with device detection and valueByDevice<T>()
  • RouteManager helpers - Navigation helpers (toAuth(), logout(), loginSuccess())
  • AuthViewModel callbacks - State callbacks for on-demand feature loading
  • Simplified APIs - Removed redundant methods (emailValidator β†’ email)

πŸ“– See MIGRATION_GUIDE.md for upgrade instructions.


βœ… Quick Start Checklist

Follow these steps to adapt the template for your project:

1️⃣ Configure API URLs

Edit lib/src/config/api_config.dart:

static const String _stagingUrl = 'https://your-staging-api.com';
static const String _productionUrl = 'https://your-production-api.com';

static const String signInUrl = '$baseUrl/api/auth/signin';
static const String signUpUrl = '$baseUrl/api/auth/signup';
// Add your custom endpoints here

2️⃣ Initialize Storage

In lib/src/main.dart, ensure storage is initialized:

await AppStorageService.instance.initialize();

3️⃣ Update User Model

Edit lib/src/modules/auth/data/models/user.dart to match your API response:

class UserModel {
  final String id;
  final String email;
  // Add your fields here

  factory UserModel.fromJson(Map<String, dynamic> json) => UserModel(
    id: json["id"],
    email: json["email"],
    // Map your fields
  );
}

4️⃣ Create Your First Module

Use the posts/ module as a reference:

  1. Copy the posts module structure
  2. Rename files and classes
  3. Update the model to match your API response
  4. Update the service to call your endpoint
  5. Add module to lib/src/utils/binding.dart

5️⃣ Customize UI

  • Replace app name in pubspec.yaml
  • Update Material 3 seed color in theme module
  • Customize login/register pages
  • Add your app logo and assets

6️⃣ Test

  • Run tests: flutter test
  • Check analyzer: flutter analyze
  • Test on devices/emulators

🀝 Contributing

Contributions are welcome! Help make this template better for everyone.

πŸ“‹ How to Contribute

  1. πŸ› Report Issues: Found a bug? Open an issue with:

    • Clear description of the problem
    • Steps to reproduce
    • Expected vs actual behavior
    • Screenshots if applicable
  2. πŸ’‘ Suggest Features: Have an idea? Open a feature request explaining:

    • Use case and problem it solves
    • Proposed solution
    • Alternative approaches considered
  3. πŸ”§ Submit Pull Requests:

    • Fork the repository
    • Create a feature branch (git checkout -b feature/amazing-feature)
    • Follow coding guidelines (see docs/architecture/coding_guidelines.md)
    • Write tests for new functionality
    • Run flutter analyze and flutter test before committing
    • Commit with descriptive messages
    • Push to your fork and submit a PR

πŸ“ PR Requirements

  • βœ… Code follows existing style and conventions
  • βœ… All tests pass (flutter test)
  • βœ… No analyzer warnings (flutter analyze)
  • βœ… New features include tests
  • βœ… Documentation updated (README, docs/, DartDoc comments)
  • βœ… Commit messages are descriptive

🎯 Areas We'd Love Help With

  • πŸ“ More documentation and examples
  • πŸ§ͺ Additional test coverage
  • 🌐 More localization languages
  • 🎨 UI/UX improvements
  • πŸ› Bug fixes and performance improvements

πŸ“– Coding Standards

Please read docs/architecture/coding_guidelines.md before contributing. Key points:

  • Use MVVM + Actions architecture
  • Follow naming conventions (plural folders, matching bindings)
  • Write DartDoc comments for public APIs
  • Keep actions/, controllers/, services/ separate
  • Use ApiResponse<T> for async state

πŸ‘€ Author & Acknowledgments

πŸ‘¨β€πŸ’» Author

Mohamed Elamin (FIFTY)

πŸ™ Acknowledgments

This template stands on the shoulders of giants:

  • GetX by Jonny Borges - Reactive state management and DI
  • Mason CLI by Felix Angelov - Code generation tool

πŸ’– Support

If this template helped you, consider:

  • ⭐ Starring the repository
  • πŸ› Reporting issues
  • πŸ”§ Contributing improvements
  • πŸ“’ Sharing with others

πŸ“„ License

MIT License

Copyright (c) 2024 Kalvad Technologies

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


πŸ”— Resources

🎯 GetX Resources

πŸ› οΈ Tools


⭐ Star this repo if you found it helpful!

πŸ› Report Issues | πŸ’‘ Request Features | 🀝 Contribute

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages