-
-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Welcome to the Flutter GetX MVVM Clean Architecture Template Wiki! This guide is designed to help you understand, set up, and utilize the template effectively for building scalable and maintainable Flutter applications. Whether you're a junior developer or someone new to Clean Architecture and GetX, this wiki will provide you with the necessary insights to get started.
- Introduction
- Getting Started
- Folder Structure
- Packages Used
- Dependency Injection
- Routing with GetX
- State Management
- Models and JSON Serialization
- Services
- Controllers
- Views / Pages
- Error Handling
- Theming
- Running the Project
- Generating JSON Serialization Code
- Additional Configurations
- Conclusion
- Contact & Support
This Flutter GetX MVVM Clean Architecture Template serves as a robust foundation for developing Flutter applications. It integrates GetX for state management and routing, get_it for dependency injection, and adheres to the MVVM (Model-View-ViewModel) architecture combined with Clean Architecture principles. This structure promotes Object-Oriented Programming (OOP) and SOLID principles, ensuring that your codebase remains organized, scalable, and maintainable.
- MVVM Architecture: Separates the UI from business logic for cleaner code.
- Clean Architecture: Promotes separation of concerns and scalability.
- GetX Integration: Simplifies state management, routing, and dependency injection.
- Dependency Injection with get_it: Manages dependencies efficiently.
-
Local Storage: Implements
get_storage
andflutter_secure_storage
for data persistence. -
Notifications: Handles local notifications on both Android and iOS using
flutter_local_notifications
. - Error Handling: Centralizes error management for better debugging and user experience.
- Theming: Supports light and dark themes with easy customization.
Before diving into the template, ensure you have the following installed on your machine:
- Flutter SDK: Installation Guide
- Dart SDK: Comes bundled with Flutter.
- IDE: Visual Studio Code, Android Studio, or any other preferred IDE.
- Device/Emulator: Set up an Android/iOS emulator or use a physical device.
Start by cloning the repository to your local machine:
git clone https://github.com/your-username/flutter-getx-clean-architecture-template.git cd flutter-getx-clean-architecture-template
Navigate to the project directory and install the required packages:
flutter pub get
This project uses json_serializable
for JSON parsing. Generate the necessary serialization code by running:
flutter pub run build_runner build --delete-conflicting-outputs
Ensure that you have a device connected or an emulator running, then execute:
flutter run
A well-organized folder structure is vital for maintaining a scalable and manageable codebase. Here's an overview of the project's structure:
vbnetlib/ ├── core/ │ ├── error/ │ │ ├── exceptions.dart │ │ └── failures.dart │ ├── middleware/ │ │ └── auth_middleware.dart │ ├── services/ │ │ ├── api_service.dart │ │ ├── api_service_impl.dart │ │ ├── connectivity_service.dart │ │ ├── connectivity_service_impl.dart │ │ ├── notification_service.dart │ │ └── storage_service.dart │ ├── utils/ │ │ ├── logger.dart │ │ └── theme.dart │ └── constants/ │ └── constants.dart ├── data/ │ ├── datasources/ │ │ ├── remote_data_source.dart │ │ └── remote_data_source_impl.dart │ ├── models/ │ │ └── user_model.dart │ └── repositories/ │ └── user_repository_impl.dart ├── domain/ │ ├── entities/ │ │ └── user.dart │ ├── repositories/ │ │ └── user_repository.dart │ └── usecases/ │ └── get_users_usecase.dart ├── presentation/ │ ├── bindings/ │ │ ├── home_binding.dart │ │ └── initial_binding.dart │ ├── controllers/ │ │ └── user_controller.dart │ ├── pages/ │ │ ├── home_page.dart │ │ └── splash_page.dart │ ├── widgets/ │ │ └── user_list_widget.dart │ └── utils/ │ └── responsive.dart ├── routes/ │ ├── app_pages.dart │ └── app_routes.dart ├── injection_container.dart └── main.dart
core/: Contains fundamental components like error handling, services, utilities, middleware, and constants used across the entire application.
data/: Manages data-related tasks, including data sources (e.g., API interactions), models (data representations), and repository implementations that fetch data from various sources.
domain/: Encapsulates the business logic, including entities (core data structures), repository interfaces, and use cases (application-specific business rules).
presentation/: Handles the UI layer, including bindings (dependency injection for views), controllers (state management), pages (UI screens), widgets (reusable UI components), and utility classes.
routes/: Manages application routing using GetX, defining pages and route names.
injection_container.dart: Sets up and registers all dependencies using
get_it
.main.dart: The entry point of the application, initializing dependencies and running the app.
The project leverages several Flutter packages to implement its features efficiently. Here's a breakdown of the primary packages used:
Package | Version | Purpose -- | -- | -- get | ^4.6.1 | State management, dependency injection, and routing solution. get_storage | ^2.0.3 | Lightweight key-value storage for simple data persistence. dio | ^4.0.0 | Powerful HTTP client for making API requests. json_annotation | ^4.0.1 | Annotations to generate JSON serialization code for models. get_it | ^7.2.0 | Service locator for dependency injection, managing app-wide dependencies. connectivity_plus | ^4.0.1 | Detects network connectivity status changes. flutter_local_notifications | ^12.0.4 | Manages local notifications on both Android and iOS. logger | ^1.2.0 | Provides easy-to-use logging capabilities. equatable | ^2.0.5 | Simplifies value comparisons, particularly for state management. intl | ^0.18.1 | Internationalization and localization support. flutter_secure_storage | ^8.0.0 | Secure storage for sensitive data like tokens and credentials. flutter_test | SDK | Testing framework for Flutter applications. flutter_lints | ^3.0.0 | Linting rules to enforce code quality and consistency. build_runner | ^2.1.4 | Automates code generation tasks, such as JSON serialization. json_serializable | ^6.0.1 | Generates JSON serialization boilerplate code based on model annotations.Dependency Injection (DI) is a design pattern that allows a class to receive its dependencies from external sources rather than creating them itself. This promotes loose coupling and makes the code more testable and maintainable.
This template utilizes get_it
as the service locator for managing dependencies across the application.
File: lib/injection_container.dart
dartimport 'package:get_it/get_it.dart'; import 'package:dio/dio.dart'; import 'package:logger/logger.dart'; import 'package:get_storage/get_storage.dart';
// Import your services, datasources, repositories, usecases, controllers, etc. import 'core/services/api_service.dart'; import 'core/services/api_service_impl.dart'; import 'core/services/connectivity_service.dart'; import 'core/services/connectivity_service_impl.dart'; import 'core/services/notification_service.dart'; import 'core/services/storage_service.dart'; import 'data/datasources/remote_data_source.dart'; import 'data/datasources/remote_data_source_impl.dart'; import 'data/repositories/user_repository_impl.dart'; import 'domain/repositories/user_repository.dart'; import 'domain/usecases/get_users_usecase.dart'; import 'presentation/controllers/user_controller.dart';
final sl = GetIt.instance;
Future<void> init() async { // Core sl.registerLazySingleton(() => Logger()); sl.registerLazySingleton(() => Dio());
// Services sl.registerLazySingleton<ApiService>(() => ApiServiceImpl(dio: sl())); sl.registerLazySingleton<ConnectivityService>(() => ConnectivityServiceImpl());
// Data Sources sl.registerLazySingleton<RemoteDataSource>(() => RemoteDataSourceImpl(apiService: sl()));
// Repositories sl.registerLazySingleton<UserRepository>(() => UserRepositoryImpl(remoteDataSource: sl()));
// Use Cases sl.registerLazySingleton<GetUsersUseCase>(() => GetUsersUseCase(repository: sl()));
// Controllers sl.registerFactory<UserController>(() => UserController(getUsersUseCase: sl()));
// Notification Service sl.registerLazySingleton<NotificationService>(() => NotificationService());
// Storage Service sl.registerLazySingleton<StorageService>(() => StorageService());
// Initialize GetStorage await sl.registerSingletonAsync<GetStorage>(() async { await GetStorage.init(); return GetStorage(); }); }
Singletons: For services that should have a single instance throughout the app's lifecycle (e.g.,
Logger
,Dio
,ApiService
), useregisterLazySingleton
.Factories: For objects that should be recreated each time they are requested (e.g.,
UserController
), useregisterFactory
.
Ensure that get_it
is initialized before running the app. This is typically done in the main.dart
file.
GetX simplifies routing by allowing you to define routes and navigate between pages effortlessly. It supports both mobile and web platforms, ensuring seamless navigation across different devices.
Routes are defined in the routes/
folder, primarily in app_pages.dart
and app_routes.dart
.
Maps route names to their corresponding pages and bindings.
File: lib/routes/app_pages.dart
dartimport 'package:get/get.dart';
import '../presentation/pages/home_page.dart'; import '../presentation/pages/splash_page.dart'; import 'app_routes.dart'; import '../presentation/bindings/initial_binding.dart';
class AppPages { static const initial = Routes.SPLASH;
static final routes = [ GetPage( name: Routes.SPLASH, page: () => SplashPage(), binding: InitialBinding(), // Register initial dependencies ), GetPage( name: Routes.HOME, page: () => HomePage(), binding: HomeBinding(), ), // Add more GetPage entries here as needed ]; }
Contains the Routes
class with static constants representing each route name.
File: lib/routes/app_routes.dart
dartabstract class Routes { static const SPLASH = '/splash'; static const HOME = '/home'; // Add more route constants here }
Use GetX's navigation methods to move between pages.
-
Navigate to Home Page:
dartGet.toNamed(Routes.HOME);
-
Navigate and Remove Previous Routes (e.g., after Splash):
dartGet.offNamed(Routes.HOME);
-
Navigate with Arguments:
dartGet.toNamed(Routes.DETAIL, arguments: {'id': userId});
-
Handling Unknown Routes:
Define an unknown route to handle navigation to undefined routes.
dartGetMaterialApp( // ... other configurations unknownRoute: GetPage( name: '/notfound', page: () => NotFoundPage(), ), );
Bindings are used to inject dependencies when navigating to a route.
-
Initial Binding:
Registers global dependencies when the app starts.
File:
lib/presentation/bindings/initial_binding.dart
dartimport 'package:get/get.dart'; import '../../core/services/api_service.dart'; import '../../core/services/connectivity_service.dart'; import '../../core/services/notification_service.dart'; import '../../core/services/storage_service.dart'; import '../../data/datasources/remote_data_source.dart'; import '../../data/datasources/remote_data_source_impl.dart'; import '../../data/repositories/user_repository_impl.dart'; import '../../domain/repositories/user_repository.dart'; import '../../domain/usecases/get_users_usecase.dart'; import '../controllers/user_controller.dart';
class InitialBinding extends Bindings { @override void dependencies() { // Register all global dependencies here // This is handled in get_it, so InitialBinding may remain empty or handle additional dependencies } }
-
Home Binding:
Injects dependencies specific to the Home page.
File:
lib/presentation/bindings/home_binding.dart
dartimport 'package:get/get.dart'; import '../controllers/user_controller.dart'; import '../../injection_container.dart' as di;
class HomeBinding extends Bindings { @override void dependencies() { Get.lazyPut<UserController>( () => UserController(getUsersUseCase: di.sl<GetUsersUseCase>()), ); } }
State management is crucial for maintaining the state of your application across different widgets and pages. This template utilizes GetX for efficient and straightforward state management.
Controllers in GetX extend GetxController
and manage the state and business logic for specific parts of the app.
File: lib/presentation/controllers/user_controller.dart
dartimport 'package:get/get.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart'; import 'package:flutter_getx_clean_architecture/domain/usecases/get_users_usecase.dart';
class UserController extends GetxController { final GetUsersUseCase getUsersUseCase;
UserController({required this.getUsersUseCase});
var users = <User>[].obs; var isLoading = false.obs; var error = ''.obs;
@override void onInit() { super.onInit(); fetchUsers(); }
Future<void> fetchUsers() async { try { isLoading.value = true; var fetchedUsers = await getUsersUseCase(); users.assignAll(fetchedUsers); } catch (e) { error.value = e.toString(); } finally { isLoading.value = false; } } }
-
users
: An observable list ofUser
entities that the UI listens to for changes. -
isLoading
: An observable boolean indicating whether data is being fetched. -
error
: An observable string that holds any error messages. -
fetchUsers()
: A method that retrieves users using theGetUsersUseCase
and updates the observables accordingly.
Controllers are accessed using Get.find()
within the view components.
Example: lib/presentation/pages/home_page.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/user_controller.dart';
class HomePage extends StatelessWidget { final UserController controller = Get.find<UserController>();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Users'), ), body: Obx(() { if (controller.isLoading.value) { return Center(child: CircularProgressIndicator()); }
if (controller.error.isNotEmpty) { return Center(child: Text(controller.error.value)); } return ListView.builder( itemCount: controller.users.length, itemBuilder: (context, index) { final user = controller.users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ); }), );
} }
-
Obx
Widget: Rebuilds its child whenever any of the observables (isLoading
,error
,users
) change. - Conditional Rendering: Displays a loading indicator, error message, or the list of users based on the current state.
Models represent the data structures used within the app. This template uses json_serializable
to automate JSON parsing and serialization.
File: lib/data/models/user_model.dart
dartimport 'package:json_annotation/json_annotation.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart';
part 'user_model.g.dart';
@JsonSerializable() class UserModel { final int id; final String name; final String username; final String email;
UserModel({ required this.id, required this.name, required this.username, required this.email, });
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
// Convert UserModel to User entity User toEntity() { return User( id: id, name: name, username: username, email: email, ); } }
-
Annotations:
@JsonSerializable()
tells thejson_serializable
package to generate serialization code. -
fromJson
andtoJson
: Factory methods generated bybuild_runner
to parse JSON data. -
toEntity()
: ConvertsUserModel
to the domain entityUser
.
After defining your models, generate the serialization code by running:
flutter pub run build_runner build --delete-conflicting-outputs
This command generates the user_model.g.dart
file containing the fromJson
and toJson
methods.
Services encapsulate reusable functionalities like API interactions, connectivity monitoring, notifications, and storage. This template includes several services to handle these aspects efficiently.
Handles all API interactions using Dio.
File: lib/core/services/api_service.dart
dartabstract class ApiService { Future<dynamic> get(String url, {Map<String, dynamic>? queryParameters}); // Add more methods like post, put, delete as needed }
File: lib/core/services/api_service_impl.dart
dartimport 'package:dio/dio.dart'; import 'api_service.dart'; import '../error/exceptions.dart';
class ApiServiceImpl implements ApiService { final Dio dio;
ApiServiceImpl({required this.dio});
@override Future<dynamic> get(String url, {Map<String, dynamic>? queryParameters}) async { try { final response = await dio.get(url, queryParameters: queryParameters); return response.data; } on DioError catch (e) { // Handle Dio errors throw ServerException(e.message); } }
// Implement other methods (post, put, delete) similarly }
Monitors network connectivity status using connectivity_plus.
File: lib/core/services/connectivity_service.dart
dartabstract class ConnectivityService { Future<bool> get isConnected; }
File: lib/core/services/connectivity_service_impl.dart
dartimport 'package:connectivity_plus/connectivity_plus.dart'; import 'connectivity_service.dart';
class ConnectivityServiceImpl implements ConnectivityService { final Connectivity _connectivity = Connectivity();
@override Future<bool> get isConnected async { var result = await _connectivity.checkConnectivity(); return result != ConnectivityResult.none; } }
Manages local notifications on both Android and iOS using flutter_local_notifications.
File: lib/core/services/notification_service.dart
dartimport 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'dart:io' show Platform;
class NotificationService { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
Future<void> init(BuildContext context) async { // Android Initialization const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS Initialization final IOSInitializationSettings initializationSettingsIOS = IOSInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async { // Display an alert dialog when a notification is received in foreground showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: Text(title ?? 'Notification'), content: Text(body ?? ''), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('OK'), ), ], ), ); }, ); // Combine Initialization Settings final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, macOS: null, // Add macOS settings if needed ); // Initialize the plugin await flutterLocalNotificationsPlugin.initialize( initializationSettings, onSelectNotification: onSelectNotification, );
}
// Handle notification tapped logic Future<void> onSelectNotification(String? payload) async { if (payload != null) { // Navigate to a specific screen based on the payload // Example: // Get.toNamed('/detail', arguments: payload); } }
Future<void> showNotification( {required String title, required String body}) async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( 'your_channel_id', // Channel ID 'your_channel_name', // Channel Name channelDescription: 'your_channel_description', // Channel Description importance: Importance.max, priority: Priority.high, showWhen: false, );
const IOSNotificationDetails iOSPlatformChannelSpecifics = IOSNotificationDetails( // Add iOS-specific settings if needed // e.g., sound: 'default.wav', ); const NotificationDetails platformChannelSpecifics = NotificationDetails( android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics, ); await flutterLocalNotificationsPlugin.show( 0, // Notification ID title, body, platformChannelSpecifics, payload: 'item x', // Optional payload );
} }
Handles data persistence using get_storage and flutter_secure_storage.
File: lib/core/services/storage_service.dart
dartimport 'package:get_storage/get_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class StorageService { final GetStorage _storage = GetStorage(); final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
// Get Storage Methods dynamic read(String key) => _storage.read(key); Future<void> write(String key, dynamic value) => _storage.write(key, value); Future<void> remove(String key) => _storage.remove(key);
// Secure Storage Methods Future<void> writeSecure(String key, String value) => _secureStorage.write(key: key, value: value); Future<String?> readSecure(String key) => _secureStorage.read(key: key); Future<void> deleteSecure(String key) => _secureStorage.delete(key: key); }
Controllers manage the state and business logic of your application. They extend GetX's GetxController
and interact with use cases to perform actions.
File: lib/presentation/controllers/user_controller.dart
dartimport 'package:get/get.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart'; import 'package:flutter_getx_clean_architecture/domain/usecases/get_users_usecase.dart';
class UserController extends GetxController { final GetUsersUseCase getUsersUseCase;
UserController({required this.getUsersUseCase});
var users = <User>[].obs; var isLoading = false.obs; var error = ''.obs;
@override void onInit() { super.onInit(); fetchUsers(); }
Future<void> fetchUsers() async { try { isLoading.value = true; var fetchedUsers = await getUsersUseCase(); users.assignAll(fetchedUsers); } catch (e) { error.value = e.toString(); } finally { isLoading.value = false; } } }
-
users
: An observable list ofUser
entities that the UI listens to for changes. -
isLoading
: An observable boolean indicating whether data is being fetched. -
error
: An observable string that holds any error messages. -
fetchUsers()
: A method that retrieves users using theGetUsersUseCase
and updates the observables accordingly.
Views, also known as Pages, are the UI components of your application. They interact with controllers to display data and respond to user interactions.
File: lib/presentation/pages/splash_page.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../routes/app_pages.dart';
class SplashPage extends StatelessWidget { @override Widget build(BuildContext context) { // Navigate to Home after a delay Future.delayed(Duration(seconds: 2), () { Get.offNamed(Routes.HOME); });
return Scaffold( body: Center( child: Text( 'GetX MVVM Template', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), ), );
} }
File: lib/presentation/pages/home_page.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/user_controller.dart';
class HomePage extends StatelessWidget { final UserController controller = Get.find<UserController>();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Users'), ), body: Obx(() { if (controller.isLoading.value) { return Center(child: CircularProgressIndicator()); }
if (controller.error.isNotEmpty) { return Center(child: Text(controller.error.value)); } return ListView.builder( itemCount: controller.users.length, itemBuilder: (context, index) { final user = controller.users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ); }), );
} }
SplashPage: The initial screen displayed when the app launches. It typically shows a loading indicator or logo before navigating to the Home page.
HomePage: Displays a list of users fetched from the API. It observes the
UserController
for changes in the user list, loading state, and errors.
Models represent the data structures used within the app. This template uses json_serializable
to automate JSON parsing and serialization.
File: lib/data/models/user_model.dart
dartimport 'package:json_annotation/json_annotation.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart';
part 'user_model.g.dart';
@JsonSerializable() class UserModel { final int id; final String name; final String username; final String email;
UserModel({ required this.id, required this.name, required this.username, required this.email, });
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
// Convert UserModel to User entity User toEntity() { return User( id: id, name: name, username: username, email: email, ); } }
Annotations:
@JsonSerializable()
tells thejson_serializable
package to generate serialization code.fromJson
andtoJson
: Factory methods generated bybuild_runner
to parse JSON data.toEntity()
: ConvertsUserModel
to the domain entityUser
.
After defining your models, generate the serialization code by running:
flutter pub run build_runner build --delete-conflicting-outputs
This command generates the user_model.g.dart
file containing the fromJson
and toJson
methods.
Services encapsulate reusable functionalities like API interactions, connectivity monitoring, notifications, and storage. This template includes several services to handle these aspects efficiently.
Handles all API interactions using Dio.
File: lib/core/services/api_service.dart
dartabstract class ApiService { Future<dynamic> get(String url, {Map<String, dynamic>? queryParameters}); // Add more methods like post, put, delete as needed }
File: lib/core/services/api_service_impl.dart
dartimport 'package:dio/dio.dart'; import 'api_service.dart'; import '../error/exceptions.dart';
class ApiServiceImpl implements ApiService { final Dio dio;
ApiServiceImpl({required this.dio});
@override Future<dynamic> get(String url, {Map<String, dynamic>? queryParameters}) async { try { final response = await dio.get(url, queryParameters: queryParameters); return response.data; } on DioError catch (e) { // Handle Dio errors throw ServerException(e.message); } }
// Implement other methods (post, put, delete) similarly }
Monitors network connectivity status using connectivity_plus.
File: lib/core/services/connectivity_service.dart
dartabstract class ConnectivityService { Future<bool> get isConnected; }
File: lib/core/services/connectivity_service_impl.dart
dartimport 'package:connectivity_plus/connectivity_plus.dart'; import 'connectivity_service.dart';
class ConnectivityServiceImpl implements ConnectivityService { final Connectivity _connectivity = Connectivity();
@override Future<bool> get isConnected async { var result = await _connectivity.checkConnectivity(); return result != ConnectivityResult.none; } }
Manages local notifications on both Android and iOS using flutter_local_notifications.
File: lib/core/services/notification_service.dart
dartimport 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'dart:io' show Platform;
class NotificationService { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
Future<void> init(BuildContext context) async { // Android Initialization const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS Initialization final IOSInitializationSettings initializationSettingsIOS = IOSInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async { // Display an alert dialog when a notification is received in foreground showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: Text(title ?? 'Notification'), content: Text(body ?? ''), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('OK'), ), ], ), ); }, ); // Combine Initialization Settings final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, macOS: null, // Add macOS settings if needed ); // Initialize the plugin await flutterLocalNotificationsPlugin.initialize( initializationSettings, onSelectNotification: onSelectNotification, );
}
// Handle notification tapped logic Future<void> onSelectNotification(String? payload) async { if (payload != null) { // Navigate to a specific screen based on the payload // Example: // Get.toNamed('/detail', arguments: payload); } }
Future<void> showNotification( {required String title, required String body}) async { const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( 'your_channel_id', // Channel ID 'your_channel_name', // Channel Name channelDescription: 'your_channel_description', // Channel Description importance: Importance.max, priority: Priority.high, showWhen: false, );
const IOSNotificationDetails iOSPlatformChannelSpecifics = IOSNotificationDetails( // Add iOS-specific settings if needed // e.g., sound: 'default.wav', ); const NotificationDetails platformChannelSpecifics = NotificationDetails( android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics, ); await flutterLocalNotificationsPlugin.show( 0, // Notification ID title, body, platformChannelSpecifics, payload: 'item x', // Optional payload );
} }
Handles data persistence using get_storage and flutter_secure_storage.
File: lib/core/services/storage_service.dart
dartimport 'package:get_storage/get_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class StorageService { final GetStorage _storage = GetStorage(); final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
// Get Storage Methods dynamic read(String key) => _storage.read(key); Future<void> write(String key, dynamic value) => _storage.write(key, value); Future<void> remove(String key) => _storage.remove(key);
// Secure Storage Methods Future<void> writeSecure(String key, String value) => _secureStorage.write(key: key, value: value); Future<String?> readSecure(String key) => _secureStorage.read(key: key); Future<void> deleteSecure(String key) => _secureStorage.delete(key: key); }
- get_storage: Used for simple key-value storage, suitable for non-sensitive data.
- flutter_secure_storage: Used for securely storing sensitive data like tokens and credentials.
-
Storing a Non-Sensitive Value:
dartfinal storageService = di.sl<StorageService>();
// Write await storageService.write('username', 'john_doe');
// Read var username = storageService.read('username');
// Remove await storageService.remove('username');
-
Storing a Sensitive Value:
dartfinal storageService = di.sl<StorageService>();
// Write Secure await storageService.writeSecure('auth_token', 'secure_token_value');
// Read Secure var authToken = await storageService.readSecure('auth_token');
// Delete Secure await storageService.deleteSecure('auth_token');
Controllers manage the state and business logic of your application. They extend GetX's GetxController
and interact with use cases to perform actions.
File: lib/presentation/controllers/user_controller.dart
dartimport 'package:get/get.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart'; import 'package:flutter_getx_clean_architecture/domain/usecases/get_users_usecase.dart';
class UserController extends GetxController { final GetUsersUseCase getUsersUseCase;
UserController({required this.getUsersUseCase});
var users = <User>[].obs; var isLoading = false.obs; var error = ''.obs;
@override void onInit() { super.onInit(); fetchUsers(); }
Future<void> fetchUsers() async { try { isLoading.value = true; var fetchedUsers = await getUsersUseCase(); users.assignAll(fetchedUsers); } catch (e) { error.value = e.toString(); } finally { isLoading.value = false; } } }
-
users
: An observable list ofUser
entities that the UI listens to for changes. -
isLoading
: An observable boolean indicating whether data is being fetched. -
error
: An observable string that holds any error messages. -
fetchUsers()
: A method that retrieves users using theGetUsersUseCase
and updates the observables accordingly.
Views, also known as Pages, are the UI components of your application. They interact with controllers to display data and respond to user interactions.
File: lib/presentation/pages/splash_page.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../routes/app_pages.dart';
class SplashPage extends StatelessWidget { @override Widget build(BuildContext context) { // Navigate to Home after a delay Future.delayed(Duration(seconds: 2), () { Get.offNamed(Routes.HOME); });
return Scaffold( body: Center( child: Text( 'GetX MVVM Template', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), ), );
} }
File: lib/presentation/pages/home_page.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/user_controller.dart';
class HomePage extends StatelessWidget { final UserController controller = Get.find<UserController>();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Users'), ), body: Obx(() { if (controller.isLoading.value) { return Center(child: CircularProgressIndicator()); }
if (controller.error.isNotEmpty) { return Center(child: Text(controller.error.value)); } return ListView.builder( itemCount: controller.users.length, itemBuilder: (context, index) { final user = controller.users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ); }), );
} }
SplashPage: The initial screen displayed when the app launches. It typically shows a loading indicator or logo before navigating to the Home page.
HomePage: Displays a list of users fetched from the API. It observes the
UserController
for changes in the user list, loading state, and errors.
Effective error handling enhances the user experience and simplifies debugging. This template centralizes error management to ensure consistent and informative error responses across the application.
File: lib/core/error/exceptions.dart
dartclass ServerException implements Exception { final String message;
ServerException(this.message); }
class CacheException implements Exception { final String message;
CacheException(this.message); }
File: lib/core/error/failures.dart
dartabstract class Failure { final String message;
Failure(this.message); }
class ServerFailure extends Failure { ServerFailure(String message) : super(message); }
class CacheFailure extends Failure { CacheFailure(String message) : super(message); }
File: lib/data/repositories/user_repository_impl.dart
dartimport 'package:flutter_getx_clean_architecture/core/error/exceptions.dart'; import 'package:flutter_getx_clean_architecture/core/error/failures.dart'; import 'package:flutter_getx_clean_architecture/data/datasources/remote_data_source.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart'; import 'package:flutter_getx_clean_architecture/domain/repositories/user_repository.dart';
class UserRepositoryImpl implements UserRepository { final RemoteDataSource remoteDataSource;
UserRepositoryImpl({required this.remoteDataSource});
@override Future<List<User>> getUsers() async { try { final List<UserModel> users = await remoteDataSource.fetchUsers(); return users.map((model) => model.toEntity()).toList(); } on ServerException catch (e) { throw ServerFailure(e.message); } catch (e) { throw ServerFailure('Unexpected error'); } } }
File: lib/presentation/pages/home_page.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/user_controller.dart';
class HomePage extends StatelessWidget { final UserController controller = Get.find<UserController>();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Users'), ), body: Obx(() { if (controller.isLoading.value) { return Center(child: CircularProgressIndicator()); }
if (controller.error.isNotEmpty) { return Center(child: Text(controller.error.value)); } return ListView.builder( itemCount: controller.users.length, itemBuilder: (context, index) { final user = controller.users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ); }), );
} }
Exceptions: Custom exception classes that represent different error scenarios.
Failures: Classes that encapsulate error information, used to communicate errors between layers.
Repository Error Handling: Catches exceptions thrown during data fetching and converts them into
Failure
instances.UI Error Display: Observes the
error
observable in the controller and displays error messages accordingly.
The application supports both light and dark themes, allowing users to choose their preferred appearance or follow the system's theme.
File: lib/core/utils/theme.dart
dartimport 'package:flutter/material.dart';
class AppTheme { static final ThemeData lightTheme = ThemeData( primarySwatch: Colors.blue, brightness: Brightness.light, // Define other theme properties appBarTheme: AppBarTheme( color: Colors.blue, elevation: 0, ), // Add more theming as needed );
static final ThemeData darkTheme = ThemeData( primarySwatch: Colors.blue, brightness: Brightness.dark, // Define other theme properties appBarTheme: AppBarTheme( color: Colors.black, elevation: 0, ), // Add more theming as needed ); }
File: lib/main.dart
dartimport 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'injection_container.dart' as di; import 'routes/app_pages.dart'; import 'core/utils/theme.dart'; import 'package:get_storage/get_storage.dart'; import 'package:url_strategy/url_strategy.dart'; import 'core/services/notification_service.dart';
void main() async { WidgetsFlutterBinding.ensureInitialized(); setPathUrlStrategy(); // Optional: Removes the hash (#) from URLs for web await di.init(); runApp(MyApp()); }
class MyApp extends StatelessWidget { final NotificationService _notificationService = di.sl<NotificationService>();
@override Widget build(BuildContext context) { // Initialize notifications with context if needed // _notificationService.init(context); // Adjust if necessary
return GetMaterialApp( title: 'GetX MVVM Template', debugShowCheckedModeBanner: false, initialRoute: AppPages.initial, getPages: AppPages.routes, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.system, // Can be ThemeMode.light or ThemeMode.dark // Optionally handle unknown routes unknownRoute: GetPage( name: '/notfound', page: () => NotFoundPage(), ), );
} }
You can allow users to switch themes dynamically by updating the themeMode
property.
Example:
dart// Toggle Theme void toggleTheme() { if (Get.isDarkMode) { Get.changeThemeMode(ThemeMode.light); } else { Get.changeThemeMode(ThemeMode.dark); } }
You can call this method from a button press or any other user interaction.
Follow these steps to set up and run the project on your local machine.
- Flutter SDK: Ensure you have Flutter installed. Installation Guide
- IDE: Use Android Studio, VS Code, or any other preferred IDE.
- Device/Emulator: Set up an Android/iOS emulator or use a physical device.
-
Clone the Repository
git clone https://github.com/your-username/flutter-getx-clean-architecture-template.git cd flutter-getx-clean-architecture-template
-
Install Dependencies
flutter pub get
-
Generate JSON Serialization Code
This step generates necessary code for JSON serialization based on model annotations.
flutter pub run build_runner build --delete-conflicting-outputs
-
Run the App
-
On Android Emulator or Device:
flutter run
-
On iOS Simulator or Device:
flutter run
-
On Web:
flutter run -d chrome
-
-
Missing Dependencies: Ensure all packages are installed by running
flutter pub get
. -
Build Errors: Run
flutter clean
followed byflutter pub get
andflutter run
. -
JSON Serialization Errors: Regenerate serialization code with
flutter pub run build_runner build --delete-conflicting-outputs
.
The project uses json_serializable
to handle JSON parsing automatically. To generate the necessary serialization code:
-
Ensure Models Are Annotated
Models should be annotated with
@JsonSerializable()
and includefromJson
andtoJson
methods.Example:
lib/data/models/user_model.dart
dartimport 'package:json_annotation/json_annotation.dart'; import 'package:flutter_getx_clean_architecture/domain/entities/user.dart';
part 'user_model.g.dart';
@JsonSerializable() class UserModel { final int id; final String name; final String username; final String email;
UserModel({ required this.id, required this.name, required this.username, required this.email, });
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
// Convert UserModel to User entity User toEntity() { return User( id: id, name: name, username: username, email: email, ); } }
-
Run Build Runner
Execute the following command to generate the
.g.dart
files containing serialization logic.flutter pub run build_runner build --delete-conflicting-outputs
-
Regenerate on Changes
Whenever you make changes to your models, rerun the build runner to update the serialization code.
flutter pub run build_runner build --delete-conflicting-outputs