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.
- π Philosophy
- π Conventions & Guidelines
- π Quick Start
- ποΈ Architecture
- π Template Structure
- π§© Modules
- π API Integration Pattern
- βοΈ Infrastructure Overview
- π οΈ Utilities & Helpers
- β FAQ
- π Documentation
- β Quick Start Checklist
- π€ Contributing
- π€ Author & Acknowledgments
- π License
- π Resources
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:
.obsandObx()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
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 fieldsfinal(use extensions for logic) - β²οΈ State Management: Prefer
ApiResponse<T>over scattered boolean flags (isLoading,hasError) - π Bindings: File names must match module names (
posts_bindings.dartforposts/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)?"
You can use this template in two ways:
1. Install Mason CLI:
dart pub global activate mason_cli2. Add the template:
mason add -g --source git https://github.com/KalvadTech/flutter-mvvm-actions-arch --path lib/mason/bricks/flutter_mvvm_actions3. 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/lib5. Generate from template:
mason make flutter_mvvm_actionsYou'll be prompted for:
project_name- Your project namedescription- Brief descriptionorg_name- Organization identifier (e.g., com.example)
- Clone this repository
- Copy the
lib/src/directory to your Flutter project'slib/folder - Copy desired modules (or all of them)
- Update
pubspec.yamlwith required dependencies (see below)
lib/src/core/- Core framework (bindings, routing, errors, presentation)lib/src/infrastructure/- HTTP, storage, cache serviceslib/src/config/- API configurationlib/src/utils/- Utilities and helpers
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
flutter pub get
flutter runThis 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
Base class for user intent handlers that wraps ViewModel calls with UX concerns:
- β Shows global loader overlay during async operations
- β
Catches
AppExceptionand 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');
});
}
}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)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
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';
}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
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
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');
});
}
}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);
}
}Plain Dart classes representing domain entities:
- Immutable - All fields are
final - JSON serialization -
fromJsonfactory andtoJsonmethod - 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};
}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);
}
}Pure Flutter widgets:
- Observes ViewModel via
GetX<ViewModel>orObx - No business logic - Delegates to Actions/ViewModels
- No direct service calls - Uses ViewModels only
- Form validation - Handles UI-level validation
All pre-built modules are located in the lib/src/modules/ directory.
Complete authentication flow with login/register pages, form validation, and token management.
Architecture:
AuthActions- ExtendsActionPresenterfor sign-in/sign-up/logout with loader overlay and error handlingAuthViewModel- Manages reactive auth state (checking β authenticated | notAuthenticated)AuthService- ExtendsApiServicefor HTTP calls, validates response shape, persists tokens viaAppStorageServiceUserModel- 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:
- Update API URLs in
lib/src/config/api_config.dart - Update
UserModelinlib/src/modules/auth/data/models/user.dartto match your API response - Customize
login.dartandregister.dartpages if needed
Auth Flow:
- App start:
AuthViewModel.checkSession()verifies tokens βauthenticatedornotAuthenticated - Login: View validates β
AuthActions.signIn()shows loader βAuthViewModel.signIn()calls service β tokens saved β state updated - Logout:
AuthViewModel.logout()clears tokens βnotAuthenticatedstate
π Documentation: See docs/architecture/auth_module.md for detailed architecture and testing guide.
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 changesConnectionHandler- Widget that conditionally renders content based on connection stateReachabilityService- 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.wifiandConnectivityType.mobileData= connectedConnectivityType.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.
Reference implementation demonstrating the complete API integration pattern using JSONPlaceholder public API.
Purpose:
- π Educational example of
apiFetch β ApiResponse β ApiHandlerpattern - π― 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:
PostModelwithfromJson/toJsonserializationPostServiceextendingApiServicewith GET requestsPostViewModelusingapiFetch()pattern andonInit()initializationPostsPagewithApiHandlerandRefreshIndicatorPostsBindingswith 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:
- Replace
PostModelwith your data model - Update
PostServiceto call your API endpoints - Configure URLs in
APIConfiguration - Customize
PostListTilewidget for your design - Add POST/PUT/DELETE methods as needed
π Complete Guide: See docs/examples/api_pattern_example.md
Multi-language support with dynamic language switching and persistent preferences.
Architecture:
LocalizationViewModel- Manages selected language and locale stateLocalizationService- Loads translations and handles GetX locale updatesLanguageModel- 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:
- Add language to
LocalizationViewModel.supportedLanguages - Create translation file in
lib/src/modules/locale/data/lang/<language_code>.dart - Add translation keys with values
Usage:
Text('auth.login.title'.tr) // Uses GetX translationLight and dark theme switching with Material 3 design and persistent preferences.
Architecture:
ThemeViewModel- Manages theme mode state (light/dark/system)ThemeService- Handles theme persistenceThemeSwitch- Switch widget for theme togglingThemeDrawerItem- 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 tileSimple 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
This template provides a complete, type-safe API integration pattern:
Flow: Service β apiFetch() β ApiResponse<T> β ApiHandler<T> β UI
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.
This template splits cross-cutting concerns into clear layers:
Primitives and contracts:
core/errors/-AppException,AuthException,APIExceptioncore/presentation/-ApiResponse,ActionPresenter
Concrete adapters:
ApiService- Centralizes headers, retry-after-refresh, error mapping, logging, and optional caching
Strategy-based HTTP response caching:
CacheStore- Storage interfaceCacheKeyStrategy- Cache key generationCachePolicy- Read/write rules and TTLCacheManager- 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
AppStorageService- Unified facade for device-local storagePreferencesStorage- GetStorage backend for preferencesSecureTokenStorage- 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');MemoryService is deprecated. Use AppStorageService for all storage needs.
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.
This template provides a comprehensive set of utilities to simplify common tasks, located in /src/utils/ and /src/core/.
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
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');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: 4Custom Breakpoints:
// Override defaults globally
ResponsiveUtils.mobileBreakpoint = 640;
ResponsiveUtils.tabletBreakpoint = 1024;
ResponsiveUtils.desktopBreakpoint = 1536;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.
/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.
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
Yes β most pieces are framework-agnostic; you'll just need to replace DI/observable wrappers.
What's coupled to GetX:
GetxControllerbase class in ViewModels β Replace with your state solution (Bloc, Riverpod, etc.)Get.find()dependency injection β Replace with Provider, get_it, etc..obsandObx()reactivity β Replace with your reactive primitivesGet.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.
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, ...);
}
}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);
});Automatic and transparent:
ApiServicedetects401 Unauthorizedresponse- Calls
ApiService.refreshSession()(base class implementation) with refresh token - Saves new access token to
AppStorageService - Retries original request with new token (one time only)
- If refresh fails, throws
AuthExceptionβ user logged out
Key Points:
- Token refresh is handled automatically by
ApiServicebase class AuthServicedoes 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.
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)
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);Yes! Modules are designed to be independent.
To use only Auth + Theme:
- Copy
lib/src/core/(required for all modules) - Copy
lib/src/infrastructure/(required for API/storage) - Copy
lib/src/config/(API configuration) - Copy
lib/src/utils/(utilities and helpers) - Copy
lib/src/modules/auth/andlib/src/modules/theme/ - Remove unused bindings from
lib/src/core/bindings/bindings.dart - Remove unused routes from
lib/src/core/routing/route_manager.dart
Dependency note: Auth module requires AppStorageService from infrastructure.
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
}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.
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 seamlesslyBenefits:
- β Models stay serializable and focused
- β Extensions are discoverable (autocomplete works)
- β Can add multiple extension files for different concerns
- β Follows single responsibility principle
Comprehensive documentation is available in the docs/ directory:
docs/architecture/coding_guidelines.md- Naming conventions, MVVM pattern, documentation style, module structure, utilities documentationdocs/architecture/auth_module.md- Authentication flow, state machine, dependency management with callbacks, testing guidedocs/architecture/connectivity_module.md- Connectivity states, reachability, configuration
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_GUIDE.md- Complete migration guide for v0.3.0+ with breaking changes, file structure updates, API changes, step-by-step instructions, and troubleshooting
CHANGELOG.md- Full log of changes, migrations, and deprecations
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.isTodayvsDateTimeUtils.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.
Follow these steps to adapt the template for your project:
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 hereIn lib/src/main.dart, ensure storage is initialized:
await AppStorageService.instance.initialize();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
);
}Use the posts/ module as a reference:
- Copy the posts module structure
- Rename files and classes
- Update the model to match your API response
- Update the service to call your endpoint
- Add module to
lib/src/utils/binding.dart
- Replace app name in
pubspec.yaml - Update Material 3 seed color in theme module
- Customize login/register pages
- Add your app logo and assets
- Run tests:
flutter test - Check analyzer:
flutter analyze - Test on devices/emulators
Contributions are welcome! Help make this template better for everyone.
-
π Report Issues: Found a bug? Open an issue with:
- Clear description of the problem
- Steps to reproduce
- Expected vs actual behavior
- Screenshots if applicable
-
π‘ Suggest Features: Have an idea? Open a feature request explaining:
- Use case and problem it solves
- Proposed solution
- Alternative approaches considered
-
π§ 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 analyzeandflutter testbefore committing - Commit with descriptive messages
- Push to your fork and submit a PR
- β 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
- π More documentation and examples
- π§ͺ Additional test coverage
- π More localization languages
- π¨ UI/UX improvements
- π Bug fixes and performance improvements
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
Mohamed Elamin (FIFTY)
- GitHub: @mohamed50
- Organization: Kalvad Technologies | @KalvadTech
- Email: [email protected]
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
If this template helped you, consider:
- β Starring the repository
- π Reporting issues
- π§ Contributing improvements
- π’ Sharing with others
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.
- Mason CLI - Code generation
- Flutter DevTools - Debugging and profiling
- Very Good CLI - Flutter project generation
β Star this repo if you found it helpful!
π Report Issues | π‘ Request Features | π€ Contribute
