You are a senior Flutter developer. You will be given this entire document as your specification. Your task: generate a complete, compilable Flutter project from scratch following every instruction below. Do NOT skip any file. Do NOT summarize. Generate FULL, working Dart code for every file listed. When you see placeholder names like `{{APP_NAME}}`, `{{PACKAGE_NAME}}`, `{{BASE_URL}}`, replace them with the values the user provides. If no values are provided, use the defaults in Section 1. # Flutter Project Generator — Architecture Specification > **What this document is:** A precise, machine-readable specification for an AI to generate a greenfield Flutter project. Every file path, every class, every method signature, and every pattern is defined below with copy-ready code. Follow it top-to-bottom. --- ## 1. DEFAULTS — Replace These for Your Project ``` APP_NAME = "My App" DART_PACKAGE = "my_app" ORG_IDENTIFIER = "com.example.myapp" BASE_URL = "https://api.example.com/" PRIMARY_COLOR = 0xFFED1C2B (Red) FIGMA_WIDTH = 430 FIGMA_HEIGHT = 927 SUPPORTED_LOCALES = [en-US, ar-SA] PRIMARY_FONT = "Poppins" SECONDARY_FONT = "GESSTwo" (Arabic font) ``` When the user asks you to generate this project, first ask them for these values. If they say "use defaults," use the values above. --- ## 2. COMMAND — Create the Flutter Project Run this first: ```bash flutter create --org {{ORG_IDENTIFIER}} --project-name {{DART_PACKAGE}} {{DART_PACKAGE}} cd {{DART_PACKAGE}} ``` --- ## 3. FILE: `pubspec.yaml` Generate this file EXACTLY. Do not add or remove packages. ```yaml name: {{DART_PACKAGE}} description: "{{APP_NAME}}" publish_to: 'none' version: 1.0.0+1 environment: sdk: ">=3.6.0 <4.0.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter # === Core Architecture === provider: ^6.1.5+1 get_it: ^8.2.0 dartz: ^0.10.1 equatable: ^2.0.7 # === Networking === http: ^1.5.0 connectivity_plus: ^6.1.5 # === Firebase === firebase_core: any firebase_messaging: ^15.2.10 firebase_analytics: ^11.5.1 firebase_crashlytics: ^4.3.8 # === UI === cupertino_icons: ^1.0.8 flutter_svg: ^2.2.0 cached_network_image: ^3.4.1 auto_size_text: ^3.0.0 shimmer: ^3.0.0 sizer: ^3.1.3 lottie: ^3.3.1 smooth_corner: ^1.1.1 flutter_staggered_animations: ^1.1.1 fl_chart: 1.0.0 flutter_rating_bar: ^4.0.1 # === Localization === easy_localization: ^3.0.8 intl: ^0.20.2 # === Storage === shared_preferences: ^2.5.3 path_provider: ^2.0.8 # === Device === permission_handler: ^12.0.1 local_auth: ^2.3.0 device_info_plus: ^11.5.0 image_picker: ^1.2.0 url_launcher: ^6.3.2 share_plus: ^11.1.0 # === Notifications === flutter_local_notifications: ^19.4.1 timezone: ^0.10.0 fluttertoast: ^8.2.12 # === Logging === logger: ^2.6.1 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 flutter: uses-material-design: true assets: - assets/ - assets/json/ - assets/fonts/ - assets/langs/ - assets/images/ - assets/images/svg/ - assets/images/png/ - assets/animations/ - assets/animations/lottie/ fonts: - family: Poppins fonts: - asset: assets/fonts/poppins/Poppins-SemiBold.ttf weight: 600 - asset: assets/fonts/poppins/Poppins-Medium.ttf weight: 500 - asset: assets/fonts/poppins/Poppins-Regular.ttf weight: 400 - asset: assets/fonts/poppins/Poppins-Light.ttf weight: 300 ``` > **Note:** If the user provides a SECONDARY_FONT, add its font family block here too. --- ## 4. FILE: `analysis_options.yaml` ```yaml include: package:flutter_lints/flutter.yaml linter: rules: # prefer_single_quotes: true ``` --- ## 5. FOLDER STRUCTURE — Create ALL These Directories ```bash mkdir -p lib/core/api mkdir -p lib/core/common_models mkdir -p lib/core/exceptions mkdir -p lib/core/utils mkdir -p lib/services/analytics mkdir -p lib/features mkdir -p lib/presentation/home/widgets mkdir -p lib/presentation/authentication mkdir -p lib/presentation/onboarding mkdir -p lib/routes mkdir -p lib/theme mkdir -p lib/extensions mkdir -p lib/widgets/buttons mkdir -p lib/widgets/loader mkdir -p lib/widgets/bottomsheet mkdir -p lib/widgets/bottom_navigation mkdir -p lib/widgets/shimmer mkdir -p lib/widgets/routes mkdir -p lib/generated mkdir -p assets/fonts/poppins mkdir -p assets/images/svg mkdir -p assets/images/png mkdir -p assets/animations/lottie mkdir -p assets/json mkdir -p assets/langs mkdir -p assets/sounds ``` --- ## 6. CORE LAYER — Generate Each File Exactly ### 6.1 FILE: `lib/core/enums.dart` ```dart enum ViewStateEnum { hide, idle, busy, error, busyLocal, errorLocal } enum AppEnvironmentTypeEnum { dev, uat, preProd, qa, staging, prod } enum GenderTypeEnum { male, female } enum ChipTypeEnum { success, error, alert, info, warning, lightBg, primaryRed } enum LoginTypeEnum { sms, whatsapp, face, fingerprint } enum OTPTypeEnum { sms, whatsapp, faceIDFingerprint } ``` ### 6.2 FILE: `lib/core/exceptions/api_failure.dart` ```dart import 'package:equatable/equatable.dart'; abstract class Failure extends Equatable implements Exception { final String message; const Failure(this.message); } class ServerFailure extends Failure { final String url; const ServerFailure(super.message, {this.url = ""}); @override List get props => [message]; } class DataParsingFailure extends Failure { const DataParsingFailure(super.message); @override List get props => [message]; } class ConnectivityFailure extends Failure { const ConnectivityFailure(super.message); @override List get props => [message]; } class UnAuthenticatedUserFailure extends Failure { final String url; const UnAuthenticatedUserFailure(super.message, {this.url = ""}); @override List get props => [message]; } class AppUpdateFailure extends Failure { const AppUpdateFailure(super.message); @override List get props => [message]; } class StatusCodeFailure extends Failure { const StatusCodeFailure(super.message); @override List get props => [message]; } class UnknownFailure extends Failure { final String url; const UnknownFailure(super.message, {this.url = ""}); @override List get props => [message]; } class UserIntimationFailure extends Failure { const UserIntimationFailure(super.message); @override List get props => [message]; } class MessageStatusFailure extends Failure { const MessageStatusFailure(super.message); @override List get props => [message]; } class InvalidCredentials extends Failure { const InvalidCredentials(String? message) : super(message ?? ''); @override List get props => [message]; } class LocalStorageFailure extends Failure { const LocalStorageFailure(super.message); @override List get props => [message]; } ``` ### 6.3 FILE: `lib/core/common_models/generic_api_model.dart` ```dart class GenericApiModel { final int? messageStatus; final String? errorMessage; final int? statusCode; final T? data; GenericApiModel({this.messageStatus, this.errorMessage, this.statusCode, this.data}); factory GenericApiModel.fromJson( Map json, T Function(Object? json)? fromJsonT, ) { return GenericApiModel( messageStatus: json['messageStatus'] as int?, errorMessage: json['errorMessage'] as String?, statusCode: json['statusCode'] as int?, data: fromJsonT != null ? fromJsonT(json['data']) : json['data'] as T?, ); } Map toJson(Object Function(T value)? toJsonT) { return { 'messageStatus': messageStatus, 'errorMessage': errorMessage, 'statusCode': statusCode, 'data': toJsonT != null && data != null ? toJsonT(data as T) : data, }; } } ``` ### 6.4 FILE: `lib/core/api_consts.dart` ```dart import 'package:{{DART_PACKAGE}}/core/enums.dart'; const int VERSION_ID = 1; const int CHANNEL = 3; const String IP_ADDRESS = "10.20.10.20"; const String GENERAL_ID = "Aborkan"; const int PATIENT_TYPE = 1; const int PATIENT_TYPE_ID = 1; const String SETUP_ID = "010266"; const bool IS_DENTAL_ALLOWED_BACKEND = false; class ApiConsts { static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; static String baseUrl = '{{BASE_URL}}'; static int appVersionID = VERSION_ID; static int appChannelId = CHANNEL; static String appIpAddress = IP_ADDRESS; static String appGeneralId = GENERAL_ID; static String sessionID = ""; static void setBackendURLs() { switch (appEnvironmentType) { case AppEnvironmentTypeEnum.prod: baseUrl = '{{BASE_URL}}'; break; case AppEnvironmentTypeEnum.dev: case AppEnvironmentTypeEnum.uat: case AppEnvironmentTypeEnum.preProd: case AppEnvironmentTypeEnum.qa: case AppEnvironmentTypeEnum.staging: baseUrl = '{{BASE_URL}}'; break; } } // ── Add endpoint paths here as you build features ── // static const String login = "api/auth/login"; } ``` ### 6.5 FILE: `lib/core/cache_consts.dart` ```dart class CacheConst { static const String firstLaunch = "firstLaunch"; static const String appAuthToken = "app_auth_token"; static const String loggedInUserObj = "logged_in_user_obj"; static const String pushToken = "push_token"; static const String appLanguage = 'language'; static const String isDarkMode = 'is_dark_mode'; // Add more cache keys as features are built } ``` ### 6.6 FILE: `lib/core/app_state.dart` ```dart import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; class AppState { final NavigationService navigationService; AppState({required this.navigationService}); double userLat = 0.0; double userLong = 0.0; bool isArabic() { final ctx = navigationService.navigatorKey.currentContext; if (ctx == null) return false; return EasyLocalization.of(ctx)?.locale.languageCode == "ar"; } int getLanguageID() => isArabic() ? 1 : 2; String? getLanguageCode() { final ctx = navigationService.navigatorKey.currentContext; if (ctx == null) return "en"; return EasyLocalization.of(ctx)?.locale.languageCode; } int getDeviceTypeID() => Platform.isIOS ? 1 : 2; bool isAuthenticated = false; String appAuthToken = ""; String deviceToken = ""; String voipToken = ""; String sessionId = ""; String deviceTypeID = ""; Map? authenticatedUser; void setAuthenticatedUser(Map? user) { authenticatedUser = user; isAuthenticated = user != null; } void resetLocation() { userLat = 0.0; userLong = 0.0; } } ``` ### 6.7 FILE: `lib/core/app_assets.dart` ```dart class AppAssets { static const String svgBasePath = 'assets/images/svg'; static const String pngBasePath = 'assets/images/png'; static const String lottieBasePath = 'assets/animations/lottie'; // Add asset constants here as you add files. Examples: // static const String logo = '$svgBasePath/logo.svg'; // static const String placeholder = '$pngBasePath/placeholder.png'; } ``` ### 6.8 FILE: `lib/core/app_export.dart` ```dart export '../routes/app_routes.dart'; export 'utils/size_utils.dart'; ``` ### 6.9 FILE: `lib/core/utils/size_utils.dart` ```dart import 'package:flutter/material.dart'; const num figmaDesignWidth = {{FIGMA_WIDTH}}; const num figmaDesignHeight = {{FIGMA_HEIGHT}}; class SizeUtils { static late double width; static late double height; static late DeviceType deviceType; static void init(BoxConstraints constraints, Orientation orientation) { width = constraints.maxWidth; height = constraints.maxHeight; deviceType = width > 600 ? DeviceType.tablet : DeviceType.phone; } } enum DeviceType { phone, tablet } bool get isTablet => SizeUtils.deviceType == DeviceType.tablet; extension ResponsiveExtension on num { double get _width => SizeUtils.width; double get _height => SizeUtils.height; double get f { double scale = (_width < _height ? _width : _height) / figmaDesignWidth; double clamp = isTablet ? 1.4 : 1.2; if (scale > clamp) scale = clamp; return this * scale; } double get w => (this * _width) / figmaDesignWidth; double get h => (this * _height) / figmaDesignHeight; double get r => (this * _width) / figmaDesignWidth; } ``` ### 6.10 FILE: `lib/core/utils/loading_utils.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; class LoadingUtils { static bool isLoading = false; static OverlayEntry? _overlayEntry; static void showFullScreenLoader() { final context = getIt.get().navigatorKey.currentContext; if (context == null || isLoading) return; isLoading = true; _overlayEntry = OverlayEntry( builder: (_) => Container( color: Colors.black38, child: const Center(child: CircularProgressIndicator()), ), ); Overlay.of(context).insert(_overlayEntry!); } static void hideFullScreenLoader() { _overlayEntry?.remove(); _overlayEntry = null; isLoading = false; } } ``` ### 6.11 FILE: `lib/core/api/api_client.dart` ```dart import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:{{DART_PACKAGE}}/core/api_consts.dart'; import 'package:{{DART_PACKAGE}}/core/app_state.dart'; import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; abstract class ApiClient { Future post( String endPoint, { required Map body, required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, bool isExternal = false, Map? apiHeaders, }); Future get( String endPoint, { required Function(dynamic response, int statusCode) onSuccess, required Function(String error, int statusCode) onFailure, Map? queryParams, Map? apiHeaders, bool isExternal = false, }); } class ApiClientImp implements ApiClient { final AppState _appState; ApiClientImp({required AppState appState}) : _appState = appState; Map _defaultHeaders() => {'Content-Type': 'application/json', 'Accept': 'application/json'}; void _injectCommonFields(Map body) { body['VersionID'] = ApiConsts.appVersionID.toString(); body['Channel'] = ApiConsts.appChannelId.toString(); body['IPAdress'] = ApiConsts.appIpAddress; body['generalid'] = ApiConsts.appGeneralId; body['LanguageID'] = _appState.getLanguageID().toString(); body['Latitude'] = _appState.userLat.toString(); body['Longitude'] = _appState.userLong.toString(); body['DeviceTypeID'] = _appState.deviceTypeID; if (_appState.appAuthToken.isNotEmpty) { body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; } } @override Future post( String endPoint, { required Map body, required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, bool isExternal = false, Map? apiHeaders, }) async { final url = isExternal ? endPoint : ApiConsts.baseUrl + endPoint; final headers = apiHeaders ?? _defaultHeaders(); if (!isExternal) _injectCommonFields(body); try { final response = await http.post(Uri.parse(url), headers: headers, body: jsonEncode(body)); final statusCode = response.statusCode; if (statusCode == 401) { onFailure('Unauthenticated', statusCode, failureType: UnAuthenticatedUserFailure('Session expired', url: url)); return; } if (statusCode == 426) { onFailure('App update required', statusCode, failureType: const AppUpdateFailure('Please update the app')); return; } if (statusCode >= 200 && statusCode < 300) { final decoded = jsonDecode(response.body); final int? messageStatus = decoded['MessageStatus']; final String? errorMessage = decoded['ErrorEndUserMessage'] ?? decoded['ErrorMessage']; if (messageStatus == 1 || messageStatus == null) { onSuccess(decoded, statusCode, messageStatus: messageStatus, errorMessage: errorMessage); } else { onFailure(errorMessage ?? 'Request failed', statusCode, messageStatus: messageStatus, failureType: MessageStatusFailure(errorMessage ?? 'Request failed with status $messageStatus')); } } else { onFailure('Server error: $statusCode', statusCode, failureType: ServerFailure('Server error: $statusCode', url: url)); } } catch (e) { if (kDebugMode) print('API POST error ($url): $e'); onFailure(e.toString(), 0, failureType: UnknownFailure(e.toString(), url: url)); } } @override Future get( String endPoint, { required Function(dynamic response, int statusCode) onSuccess, required Function(String error, int statusCode) onFailure, Map? queryParams, Map? apiHeaders, bool isExternal = false, }) async { final baseUrl = isExternal ? endPoint : ApiConsts.baseUrl + endPoint; final uri = Uri.parse(baseUrl).replace(queryParameters: queryParams?.map((k, v) => MapEntry(k, v.toString()))); final headers = apiHeaders ?? _defaultHeaders(); try { final response = await http.get(uri, headers: headers); if (response.statusCode >= 200 && response.statusCode < 300) { onSuccess(jsonDecode(response.body), response.statusCode); } else { onFailure('GET failed: ${response.statusCode}', response.statusCode); } } catch (e) { onFailure(e.toString(), 0); } } } ``` --- ## 7. SERVICES LAYER — Generate Each File Exactly ### 7.1 FILE: `lib/services/logger_service.dart` ```dart import 'package:logger/logger.dart'; abstract class LoggerService { void logError(String message); void logInfo(String message); } class LoggerServiceImp implements LoggerService { final Logger logger; LoggerServiceImp({required this.logger}); @override void logError(String message) => logger.e(message); @override void logInfo(String message) => logger.d(message); } ``` ### 7.2 FILE: `lib/services/navigation_service.dart` ```dart import 'package:flutter/material.dart'; class NavigationService { final GlobalKey navigatorKey = GlobalKey(); BuildContext? get context => navigatorKey.currentContext; Future push(Route route) => navigatorKey.currentState!.push(route); Future pushAndRemoveUntil(Route route, RoutePredicate predicate) => navigatorKey.currentState!.pushAndRemoveUntil(route, predicate); void pop([T? result]) => navigatorKey.currentState!.pop(result); void popUntilNamed(String routeName) => navigatorKey.currentState?.popUntil(ModalRoute.withName(routeName)); void replaceAllRoutesAndNavigateTo(String routeName) { navigatorKey.currentState?.pushNamedAndRemoveUntil(routeName, (route) => false); } void pushAndReplace(String routeName) => navigatorKey.currentState?.pushReplacementNamed(routeName); void pushNamed(String routeName, {Object? arguments}) => navigatorKey.currentState?.pushNamed(routeName, arguments: arguments); Future pushPage({required Widget page, bool fullscreenDialog = false}) => navigatorKey.currentState!.push( MaterialPageRoute(builder: (_) => page, fullscreenDialog: fullscreenDialog), ); } ``` ### 7.3 FILE: `lib/services/cache_service.dart` ```dart import 'dart:convert'; import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; abstract class CacheService { Future saveString({required String key, required String value}); Future saveInt({required String key, required int value}); Future saveBool({required String key, required bool value}); String? getString({required String key}); int? getInt({required String key}); bool? getBool({required String key}); Future getObject({required String key}); Future saveObject({required String key, required dynamic value}); Future remove({required String key}); Future clear(); } class CacheServiceImp implements CacheService { final SharedPreferences sharedPreferences; final LoggerService loggerService; CacheServiceImp({required this.sharedPreferences, required this.loggerService}); @override Future saveString({required String key, required String value}) async => await sharedPreferences.setString(key, value); @override Future saveInt({required String key, required int value}) async => await sharedPreferences.setInt(key, value); @override Future saveBool({required String key, required bool value}) async => await sharedPreferences.setBool(key, value); @override String? getString({required String key}) => sharedPreferences.getString(key); @override int? getInt({required String key}) => sharedPreferences.getInt(key); @override bool? getBool({required String key}) => sharedPreferences.getBool(key); @override Future getObject({required String key}) async { try { await sharedPreferences.reload(); final s = sharedPreferences.getString(key); return s == null ? null : json.decode(s); } catch (e) { loggerService.logError(e.toString()); return null; } } @override Future saveObject({required String key, required dynamic value}) async => await sharedPreferences.setString(key, json.encode(value)); @override Future remove({required String key}) async => await sharedPreferences.remove(key); @override Future clear() async => await sharedPreferences.clear(); } ``` ### 7.4 FILE: `lib/services/dialog_service.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; abstract class DialogService { Future showErrorBottomSheet({String title, required String message, Function()? onOkPressed}); Future showConfirmBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); } class DialogServiceImp implements DialogService { final NavigationService navigationService; bool _isSheetShowing = false; DialogServiceImp({required this.navigationService}); @override Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed}) async { if (_isSheetShowing) return; final context = navigationService.navigatorKey.currentContext; if (context == null) return; _isSheetShowing = true; await showModalBottomSheet( context: context, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (_) => Padding( padding: const EdgeInsets.all(24), child: Column(mainAxisSize: MainAxisSize.min, children: [ if (title.isNotEmpty) ...[Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 12)], Text(message, textAlign: TextAlign.center), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryRedColor), onPressed: () { Navigator.pop(context); onOkPressed?.call(); }, child: const Text('OK', style: TextStyle(color: Colors.white)), ), ), ]), ), ); _isSheetShowing = false; } @override Future showConfirmBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; await showModalBottomSheet( context: context, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (_) => Padding( padding: const EdgeInsets.all(24), child: Column(mainAxisSize: MainAxisSize.min, children: [ Text(message, textAlign: TextAlign.center), const SizedBox(height: 24), Row(children: [ Expanded(child: OutlinedButton(onPressed: () { Navigator.pop(context); onCancelPressed?.call(); }, child: const Text('Cancel'))), const SizedBox(width: 12), Expanded(child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryRedColor), onPressed: () { Navigator.pop(context); onOkPressed(); }, child: const Text('OK', style: TextStyle(color: Colors.white)), )), ]), ]), ), ); } } ``` ### 7.5 FILE: `lib/services/error_handler_service.dart` ```dart import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; import 'package:{{DART_PACKAGE}}/core/utils/loading_utils.dart'; import 'package:{{DART_PACKAGE}}/services/dialog_service.dart'; import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; import 'package:{{DART_PACKAGE}}/routes/app_routes.dart'; abstract class ErrorHandlerService { Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}); } class ErrorHandlerServiceImp implements ErrorHandlerService { final DialogService dialogService; final LoggerService loggerService; final NavigationService navigationService; ErrorHandlerServiceImp({required this.dialogService, required this.loggerService, required this.navigationService}); @override Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}) async { if (LoadingUtils.isLoading) LoadingUtils.hideFullScreenLoader(); if (failure is ServerFailure) { loggerService.logError("Server: ${failure.message}"); await _show(failure); } else if (failure is ConnectivityFailure) { loggerService.logError("Connectivity: ${failure.message}"); await _show(failure, title: "No Internet"); } else if (failure is UnAuthenticatedUserFailure) { loggerService.logError("Unauth: ${failure.message}"); await _show(failure, title: "Session Expired", onOk: () => navigationService.replaceAllRoutesAndNavigateTo(AppRoutes.landingScreen)); } else if (failure is AppUpdateFailure) { await _show(failure, title: "Update Required"); } else if (failure is DataParsingFailure) { loggerService.logError("Parse: ${failure.message}"); await _show(failure, title: "Data Error"); } else if (failure is UserIntimationFailure) { onUnHandledFailure != null ? onUnHandledFailure(failure) : await _show(failure, onOk: onOkPressed); } else if (failure is MessageStatusFailure) { onMessageStatusFailure != null ? onMessageStatusFailure(failure) : await _show(failure, onOk: onOkPressed); } else { loggerService.logError("Unhandled: $failure"); await _show(failure, onOk: onOkPressed); } } Future _show(Failure f, {String? title, Function()? onOk}) async => await dialogService.showErrorBottomSheet(title: title ?? "", message: f.message, onOkPressed: onOk); } ``` ### 7.6 FILE: `lib/services/firebase_service.dart` ```dart import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:{{DART_PACKAGE}}/core/app_state.dart'; import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; abstract class FirebaseService { Future getDeviceToken(); } class FirebaseServiceImpl implements FirebaseService { final FirebaseMessaging firebaseMessaging; final LoggerService loggerService; final AppState appState; FirebaseServiceImpl({required this.firebaseMessaging, required this.loggerService, required this.appState}); @override Future getDeviceToken() async { try { final token = await firebaseMessaging.getToken(); appState.deviceToken = token ?? ""; return appState.deviceToken; } catch (e) { loggerService.logError(e.toString()); return ""; } } } ``` ### 7.7 FILE: `lib/services/notification_service.dart` ```dart import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; import 'package:timezone/data/latest_all.dart' as tz; abstract class NotificationService { Future initialize({Function(String payload)? onNotificationClick}); Future showNotification({required String title, required String body, String? payload}); Future cancelAllNotifications(); } class NotificationServiceImp implements NotificationService { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; final LoggerService loggerService; NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService}); @override Future initialize({Function(String payload)? onNotificationClick}) async { tz.initializeTimeZones(); const android = AndroidInitializationSettings('app_icon'); const ios = DarwinInitializationSettings(requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true); await flutterLocalNotificationsPlugin.initialize( const InitializationSettings(android: android, iOS: ios), onDidReceiveNotificationResponse: (r) => onNotificationClick?.call(r.payload ?? ''), ); } @override Future showNotification({required String title, required String body, String? payload}) async { const details = NotificationDetails( android: AndroidNotificationDetails('general', 'General', importance: Importance.high, priority: Priority.high), iOS: DarwinNotificationDetails(), ); await flutterLocalNotificationsPlugin.show(DateTime.now().millisecondsSinceEpoch ~/ 1000, title, body, details, payload: payload); } @override Future cancelAllNotifications() async => await flutterLocalNotificationsPlugin.cancelAll(); } ``` ### 7.8 FILE: `lib/services/analytics/analytics_service.dart` ```dart import 'package:firebase_analytics/firebase_analytics.dart'; class GAnalytics { final _analytics = FirebaseAnalytics.instance; Future logEvent(String name, {Map? parameters}) async { try { await _analytics.logEvent(name: name.trim().toLowerCase(), parameters: parameters?.map((k, v) => MapEntry(k, v as Object))); } catch (_) {} } Future setUser({String? userId, String? language}) async { if (userId != null) _analytics.setUserProperty(name: 'userid', value: userId); if (language != null) _analytics.setUserProperty(name: 'user_language', value: language); } } ``` --- ## 8. THEME LAYER ### 8.1 FILE: `lib/theme/colors.dart` ```dart import 'package:flutter/material.dart'; class AppColors { static bool isDarkMode = false; static const transparent = Colors.transparent; static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8); static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFFFFFFF); static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : const Color({{PRIMARY_COLOR}}); static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); static const Color successColor = Color(0xFF18C273); static Color get textColor => isDarkMode ? dark.textColor : const Color(0xFF2E3039); static Color get iconColor => isDarkMode ? dark.iconColor : const Color(0xFF2E3039); static Color get greyColor => isDarkMode ? dark.greyColor : const Color(0xFFEFEFF0); static Color get textColorLight => isDarkMode ? dark.textColorLight : const Color(0xFF5E5E5E); static Color get dividerColor => isDarkMode ? dark.dividerColor : const Color(0x40D2D2D2); static Color get borderGrayColor => isDarkMode ? dark.borderGrayColor : const Color(0x332E3039); static Color get greyTextColor => isDarkMode ? dark.greyTextColor : const Color(0xFF8F9AA3); static Color get inputLabelTextColor => isDarkMode ? dark.inputLabelTextColor : const Color(0xFF898A8D); static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); static Color get warningColor => isDarkMode ? dark.warningColor : const Color(0xFFFFCC00); static Color get infoColor => isDarkMode ? dark.infoColor : const Color(0xFF0B85F7); static Color get shimmerBaseColor => isDarkMode ? dark.shimmerBaseColor : const Color(0xFFE0E0E0); static Color get shimmerHighlightColor => isDarkMode ? dark.shimmerHighlightColor : const Color(0xFFF5F5F5); static Color get blackColor => textColor; static Color get bgScaffoldColor => scaffoldBgColor; static const AppColorsDark dark = AppColorsDark(); } class AppColorsDark { const AppColorsDark(); Color get scaffoldBgColor => const Color(0xFF191919); Color get whiteColor => const Color(0xFF1E1E1E); Color get primaryRedColor => const Color(0xFFDE5C5D); Color get secondaryLightRedColor => const Color(0xFF3A1015); Color get textColor => const Color(0xFFFFFFFF); Color get iconColor => const Color(0xFFFFFFFF); Color get greyColor => const Color(0xFF2C2C2E); Color get textColorLight => const Color(0xFFB0B0B0); Color get dividerColor => const Color(0x33FFFFFF); Color get borderGrayColor => const Color(0x55ECECEC); Color get greyTextColor => const Color(0xFF8F9AA3); Color get inputLabelTextColor => const Color(0xFF9E9E9E); Color get errorColor => const Color(0xFFD63D48); Color get warningColor => const Color(0xFFFFCC00); Color get infoColor => const Color(0xFF4DA6FF); Color get shimmerBaseColor => const Color(0xFF2C2C2E); Color get shimmerHighlightColor => const Color(0xFF3A3A3C); } ``` ### 8.2 FILE: `lib/theme/app_theme.dart` ```dart import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; class AppTheme { static ThemeData getTheme(bool isArabic) => ThemeData( fontFamily: isArabic ? '{{SECONDARY_FONT}}' : '{{PRIMARY_FONT}}', primarySwatch: Colors.red, brightness: Brightness.light, visualDensity: VisualDensity.adaptivePlatformDensity, pageTransitionsTheme: const PageTransitionsTheme(builders: { TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), }), canvasColor: Colors.white, splashColor: Colors.transparent, appBarTheme: const AppBarTheme(elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.light, surfaceTintColor: Colors.transparent), ); static ThemeData getDarkTheme(bool isArabic) => ThemeData( fontFamily: isArabic ? '{{SECONDARY_FONT}}' : '{{PRIMARY_FONT}}', primarySwatch: Colors.red, brightness: Brightness.dark, visualDensity: VisualDensity.adaptivePlatformDensity, pageTransitionsTheme: const PageTransitionsTheme(builders: { TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), }), canvasColor: AppColors.dark.scaffoldBgColor, scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, splashColor: Colors.transparent, appBarTheme: AppBarTheme(color: AppColors.dark.scaffoldBgColor, elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.light, surfaceTintColor: Colors.transparent), ); } ``` --- ## 9. EXTENSIONS ### 9.1 FILE: `lib/extensions/int_extensions.dart` ```dart import 'package:flutter/material.dart'; extension IntExtensions on int { Widget get height => SizedBox(height: toDouble()); Widget get width => SizedBox(width: toDouble()); Widget get divider => Divider(height: toDouble(), thickness: toDouble(), color: const Color(0x30D2D2D2)); } ``` ### 9.2 FILE: `lib/extensions/string_extensions.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/core/utils/size_utils.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; extension CapExtension on String { String get inCaps => isEmpty ? '' : '${this[0].toUpperCase()}${substring(1)}'; String get capitalizeFirstOfEach => trim().isEmpty ? '' : trim().toLowerCase().split(' ').map((s) => s.inCaps).join(' '); } extension StringToWidget on String { Widget _text(double size, {Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, bool isUnderLine = false, int? maxLines, TextOverflow? overflow, double letterSpacing = 0}) { return Text(this, textAlign: isCenter ? TextAlign.center : null, maxLines: maxLines, overflow: overflow, style: TextStyle(fontSize: size.f, fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: letterSpacing, decoration: isUnderLine ? TextDecoration.underline : null, decorationColor: color ?? AppColors.blackColor), ); } Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxLines, TextOverflow? overflow}) => _text(8, color: color, fontWeight: fontWeight, isBold: isBold, maxLines: maxLines, overflow: overflow); Widget toText10({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(10, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); Widget toText11({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines}) => _text(11, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines); Widget toText12({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(12, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); Widget toText13({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(13, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); Widget toText14({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(14, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); Widget toText16({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(16, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); Widget toText18({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines}) => _text(18, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines); Widget toText20({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(20, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); Widget toText24({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(24, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); Widget toText30({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(30, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); } ``` ### 9.3 FILE: `lib/extensions/widget_extensions.dart` ```dart import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; extension WidgetExtensions on Widget { Widget onPress(VoidCallback onTap) => InkWell(onTap: onTap, child: this); Widget get expanded => Expanded(child: this); Widget get center => Center(child: this); Widget circle(double v) => ClipRRect(borderRadius: BorderRadius.circular(v), child: this); Widget paddingAll(double v) => Padding(padding: EdgeInsets.all(v), child: this); Widget paddingSymmetrical(double h, double v) => Padding(padding: EdgeInsets.symmetric(horizontal: h, vertical: v), child: this); Widget paddingOnly({double left = 0, double right = 0, double top = 0, double bottom = 0}) => Padding(padding: EdgeInsetsDirectional.only(start: left, end: right, top: top, bottom: bottom), child: this); Widget toShimmer({bool isShow = true}) => isShow ? Shimmer.fromColors(baseColor: AppColors.shimmerBaseColor, highlightColor: AppColors.shimmerHighlightColor, child: this) : this; Widget objectContainerView({double radius = 15}) => Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(radius), boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.15), blurRadius: 26, offset: const Offset(0, -3))]), child: this, ); } ``` ### 9.4 FILE: `lib/extensions/context_extensions.dart` ```dart import 'package:flutter/material.dart'; extension ContextUtils on BuildContext { double get screenHeight => MediaQuery.of(this).size.height; double get screenWidth => MediaQuery.of(this).size.width; ThemeData get theme => Theme.of(this); TextTheme get textTheme => theme.textTheme; } extension ShowBottomSheetExt on BuildContext { Future showSheet({required Widget child, bool isScrollControlled = true, bool isDismissible = true}) { return showModalBottomSheet(context: this, isScrollControlled: isScrollControlled, isDismissible: isDismissible, backgroundColor: Colors.transparent, builder: (_) => child); } } ``` ### 9.5 FILE: `lib/extensions/route_extensions.dart` ```dart import 'package:flutter/material.dart'; extension NavigationExtensions on BuildContext { void navigateWithName(String routeName, {Object? arguments}) => Navigator.pushNamed(this, routeName, arguments: arguments); Future navigateReplaceWithName(String routeName, {Object? arguments}) async => await Navigator.pushReplacementNamed(this, routeName, arguments: arguments); void navigateAndRemoveAll(String routeName) => Navigator.pushNamedAndRemoveUntil(this, routeName, (route) => false); void pop() => Navigator.of(this).pop(); void pushTo(Widget page) => Navigator.push(this, MaterialPageRoute(builder: (_) => page)); } ``` --- ## 10. WIDGETS — Starter Widgets ### 10.1 FILE: `lib/widgets/buttons/custom_button.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/core/utils/size_utils.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; class CustomButton extends StatelessWidget { final String label; final VoidCallback? onPressed; final Color? color; final Color? textColor; final double? width; final bool isOutlined; final bool isLoading; const CustomButton({super.key, required this.label, this.onPressed, this.color, this.textColor, this.width, this.isOutlined = false, this.isLoading = false}); @override Widget build(BuildContext context) { final bg = color ?? AppColors.primaryRedColor; return SizedBox( width: width ?? double.infinity, height: 48.h, child: isOutlined ? OutlinedButton(onPressed: isLoading ? null : onPressed, style: OutlinedButton.styleFrom(side: BorderSide(color: bg), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))), child: _child(bg)) : ElevatedButton(onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom(backgroundColor: bg, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))), child: _child(textColor ?? Colors.white)), ); } Widget _child(Color c) => isLoading ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2, color: c)) : Text(label, style: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, color: c)); } ``` ### 10.2 FILE: `lib/widgets/routes/fade_page.dart` ```dart import 'package:flutter/material.dart'; class FadePage extends PageRouteBuilder { final Widget page; FadePage({required this.page}) : super( pageBuilder: (_, __, ___) => page, transitionsBuilder: (_, a, __, child) => FadeTransition(opacity: a, child: child), transitionDuration: const Duration(milliseconds: 300), ); } ``` --- ## 11. DEPENDENCY INJECTION — `lib/core/dependencies.dart` ```dart import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get_it/get_it.dart'; import 'package:logger/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:{{DART_PACKAGE}}/core/api/api_client.dart'; import 'package:{{DART_PACKAGE}}/core/app_state.dart'; import 'package:{{DART_PACKAGE}}/services/analytics/analytics_service.dart'; import 'package:{{DART_PACKAGE}}/services/cache_service.dart'; import 'package:{{DART_PACKAGE}}/services/dialog_service.dart'; import 'package:{{DART_PACKAGE}}/services/error_handler_service.dart'; import 'package:{{DART_PACKAGE}}/services/firebase_service.dart'; import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; import 'package:{{DART_PACKAGE}}/services/notification_service.dart'; final GetIt getIt = GetIt.instance; class AppDependencies { static Future addDependencies() async { final logger = Logger(printer: PrettyPrinter(methodCount: 2, errorMethodCount: 5, lineLength: 1000, colors: true, printEmojis: true)); getIt.registerLazySingleton(() => LoggerServiceImp(logger: logger)); getIt.registerLazySingleton(() => NavigationService()); getIt.registerLazySingleton(() => AppState(navigationService: getIt())); getIt.registerLazySingleton(() => GAnalytics()); getIt.registerLazySingleton(() => DialogServiceImp(navigationService: getIt())); getIt.registerLazySingleton(() => ErrorHandlerServiceImp(dialogService: getIt(), loggerService: getIt(), navigationService: getIt())); final sp = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => CacheServiceImp(sharedPreferences: sp, loggerService: getIt())); getIt.registerLazySingleton(() => ApiClientImp(appState: getIt())); getIt.registerLazySingleton(() => FirebaseServiceImpl(firebaseMessaging: FirebaseMessaging.instance, loggerService: getIt(), appState: getIt())); final flnp = FlutterLocalNotificationsPlugin(); getIt.registerLazySingleton(() => NotificationServiceImp(flutterLocalNotificationsPlugin: flnp, loggerService: getIt())); // ═══ Register feature repos & ViewModels below as you add them ═══ } } ``` --- ## 12. ROUTING — `lib/routes/app_routes.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/presentation/home/navigation_screen.dart'; import 'package:{{DART_PACKAGE}}/splash_page.dart'; class AppRoutes { static const String initialRoute = '/'; static const String landingScreen = '/landingScreen'; // Add route names as you build features: // static const String loginScreen = '/loginScreen'; static Map get routes => { initialRoute: (_) => const SplashPage(), landingScreen: (_) => const LandingNavigation(), }; } ``` --- ## 13. LOCALIZATION FILES ### FILE: `assets/langs/en-US.json` ```json { "appName": "{{APP_NAME}}", "home": "Home", "services": "Services", "medicalFile": "Medical File", "bookAppointment": "Book Appointment", "login": "Login", "register": "Register", "cancel": "Cancel", "confirm": "Confirm", "done": "Done", "ok": "OK", "loading": "Loading...", "noDataAvailable": "No data available", "noInternetConnection": "No internet connection", "settings": "Settings", "language": "Language", "darkMode": "Dark Mode", "logout": "Logout", "search": "Search" } ``` ### FILE: `assets/langs/ar-SA.json` ```json { "appName": "{{APP_NAME}}", "home": "الرئيسية", "services": "الخدمات", "medicalFile": "الملف الطبي", "bookAppointment": "حجز موعد", "login": "تسجيل الدخول", "register": "التسجيل", "cancel": "إلغاء", "confirm": "تأكيد", "done": "تم", "ok": "حسنا", "loading": "جاري التحميل...", "noDataAvailable": "لا توجد بيانات", "noInternetConnection": "لا يوجد اتصال بالإنترنت", "settings": "الإعدادات", "language": "اللغة", "darkMode": "الوضع الداكن", "logout": "تسجيل الخروج", "search": "بحث" } ``` --- ## 14. PRESENTATION — Starter Screens ### FILE: `lib/splash_page.dart` ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/core/api_consts.dart'; import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; import 'package:{{DART_PACKAGE}}/presentation/home/navigation_screen.dart'; import 'package:{{DART_PACKAGE}}/services/notification_service.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; import 'package:{{DART_PACKAGE}}/widgets/routes/fade_page.dart'; class SplashPage extends StatefulWidget { const SplashPage({super.key}); @override State createState() => _SplashPageState(); } class _SplashPageState extends State { @override void initState() { super.initState(); _init(); } Future _init() async { ApiConsts.setBackendURLs(); await getIt.get().initialize(); Timer(const Duration(seconds: 2), () { Navigator.of(context).pushReplacement(FadePage(page: const LandingNavigation())); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.whiteColor, body: Center(child: Text('{{APP_NAME}}', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: AppColors.primaryRedColor))), ); } } ``` ### FILE: `lib/presentation/home/navigation_screen.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/presentation/home/landing_page.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; class LandingNavigation extends StatefulWidget { const LandingNavigation({super.key}); @override State createState() => _LandingNavigationState(); } class _LandingNavigationState extends State { int _idx = 0; final _pc = PageController(); final _pages = const [ LandingPage(), Center(child: Text('Medical File')), Center(child: Text('Book Appointment')), Center(child: Text('Services')), ]; @override Widget build(BuildContext context) { return Scaffold( body: PageView(controller: _pc, physics: const NeverScrollableScrollPhysics(), children: _pages), bottomNavigationBar: BottomNavigationBar( currentIndex: _idx, type: BottomNavigationBarType.fixed, selectedItemColor: AppColors.primaryRedColor, unselectedItemColor: AppColors.greyTextColor, onTap: (i) { setState(() => _idx = i); _pc.jumpToPage(i); }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.folder_outlined), activeIcon: Icon(Icons.folder), label: 'File'), BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Book'), BottomNavigationBarItem(icon: Icon(Icons.grid_view_outlined), activeIcon: Icon(Icons.grid_view), label: 'Services'), ], ), ); } } ``` ### FILE: `lib/presentation/home/landing_page.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/extensions/int_extensions.dart'; import 'package:{{DART_PACKAGE}}/extensions/string_extensions.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; class LandingPage extends StatelessWidget { const LandingPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.scaffoldBgColor, body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 24.height, 'Welcome'.toText24(isBold: true), 8.height, 'What would you like to do today?'.toText14(color: AppColors.greyTextColor), 24.height, Expanded(child: Center(child: 'Home content goes here'.toText16(color: AppColors.textColorLight, isCenter: true))), ]), ), ), ); } } ``` --- ## 15. ENTRY POINT — `lib/main.dart` ```dart import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:sizer/sizer.dart'; import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; import 'package:{{DART_PACKAGE}}/routes/app_routes.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; import 'package:{{DART_PACKAGE}}/theme/app_theme.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) => super.createHttpClient(context)..badCertificateCallback = (cert, host, port) => true; } Future _init() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); // Uncomment when Firebase is configured: // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await AppDependencies.addDependencies(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); HttpOverrides.global = MyHttpOverrides(); } void main() async { await _init(); runApp( EasyLocalization( supportedLocales: const [Locale('en', 'US'), Locale('ar', 'SA')], path: 'assets/langs', fallbackLocale: const Locale('en', 'US'), child: MultiProvider( providers: [ // ═══ Add ChangeNotifierProviders as you build features ═══ ], child: const MyApp(), ), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return SafeArea( top: false, bottom: !Platform.isIOS, child: LayoutBuilder(builder: (context, constraints) { return Sizer(builder: (context, orientation, deviceType) { final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; return MaterialApp( title: '{{APP_NAME}}', debugShowCheckedModeBanner: false, localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, initialRoute: AppRoutes.initialRoute, routes: AppRoutes.routes, theme: AppTheme.getTheme(isArabic), darkTheme: AppTheme.getDarkTheme(isArabic), themeMode: AppColors.isDarkMode ? ThemeMode.dark : ThemeMode.light, navigatorKey: getIt.get().navigatorKey, builder: (context, child) => MediaQuery( data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling, alwaysUse24HourFormat: true), child: child!, ), ); }); }), ); } } ``` --- ## 16. FEATURE MODULE TEMPLATE When asked to **add a new feature**, create these four files. Replace `{{feature}}` (snake_case) and `{{Feature}}` (PascalCase). ### `lib/features/{{feature}}/{{feature}}_repo.dart` ```dart import 'package:dartz/dartz.dart'; import 'package:{{DART_PACKAGE}}/core/api/api_client.dart'; import 'package:{{DART_PACKAGE}}/core/common_models/generic_api_model.dart'; import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; abstract class {{Feature}}Repo { // Future>>> getItems(); } class {{Feature}}RepoImp implements {{Feature}}Repo { final ApiClient apiClient; final LoggerService loggerService; {{Feature}}RepoImp({required this.loggerService, required this.apiClient}); // @override // Future>>> getItems() async { // Map body = {}; // try { // GenericApiModel>? result; // Failure? failure; // await apiClient.post('endpoint', body: body, // onFailure: (e, s, {messageStatus, failureType}) => failure = failureType, // onSuccess: (r, s, {messageStatus, errorMessage}) { // try { result = GenericApiModel(data: (r['Key'] as List).map((e) => YourModel.fromJson(e)).toList()); } // catch (e) { failure = DataParsingFailure(e.toString()); } // }); // if (failure != null) return Left(failure!); // return Right(result!); // } catch (e) { return Left(UnknownFailure(e.toString())); } // } } ``` ### `lib/features/{{feature}}/{{feature}}_view_model.dart` ```dart import 'package:flutter/material.dart'; import 'package:{{DART_PACKAGE}}/features/{{feature}}/{{feature}}_repo.dart'; import 'package:{{DART_PACKAGE}}/services/error_handler_service.dart'; import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; class {{Feature}}ViewModel extends ChangeNotifier { final {{Feature}}Repo _repo; final ErrorHandlerService _errorHandler; final NavigationService _nav; {{Feature}}ViewModel({required {{Feature}}Repo repo, required ErrorHandlerService errorHandler, required NavigationService nav}) : _repo = repo, _errorHandler = errorHandler, _nav = nav; bool isLoading = false; // void init() { isLoading = true; notifyListeners(); _fetch(); } // void _fetch() async { // final r = await _repo.getItems(); // r.fold((f) { isLoading = false; _errorHandler.handleError(failure: f); notifyListeners(); }, // (d) { /* items = d.data ?? []; */ isLoading = false; notifyListeners(); }); // } } ``` ### `lib/presentation/{{feature}}/{{feature}}_page.dart` ```dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:{{DART_PACKAGE}}/features/{{feature}}/{{feature}}_view_model.dart'; import 'package:{{DART_PACKAGE}}/theme/colors.dart'; class {{Feature}}Page extends StatefulWidget { const {{Feature}}Page({super.key}); @override State<{{Feature}}Page> createState() => _{{Feature}}PageState(); } class _{{Feature}}PageState extends State<{{Feature}}Page> { @override void initState() { super.initState(); // Provider.of<{{Feature}}ViewModel>(context, listen: false).init(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: AppBar(title: const Text('{{Feature}}')), body: Consumer<{{Feature}}ViewModel>( builder: (_, vm, __) { if (vm.isLoading) return const Center(child: CircularProgressIndicator()); return const Center(child: Text('Content here')); }, ), ); } } ``` ### After creating, REGISTER: **`dependencies.dart`:** ```dart getIt.registerLazySingleton<{{Feature}}Repo>(() => {{Feature}}RepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton<{{Feature}}ViewModel>(() => {{Feature}}ViewModel(repo: getIt(), errorHandler: getIt(), nav: getIt())); ``` **`main.dart` providers list:** ```dart ChangeNotifierProvider<{{Feature}}ViewModel>(create: (_) => getIt.get<{{Feature}}ViewModel>()), ``` **`app_routes.dart`:** ```dart static const String {{feature}}Page = '/{{feature}}'; // in routes map: {{feature}}Page: (_) => const {{Feature}}Page(), ``` --- ## 17. RULES — ALWAYS FOLLOW 1. **Every service/repo = abstract class + implementation.** Never use concrete directly. 2. **All DI in `dependencies.dart`.** Only use `getIt.get()` elsewhere. 3. **All repos return `Future>>`.** 4. **All ViewModels extend `ChangeNotifier`.** Call `notifyListeners()` after state changes. 5. **ViewModels NEVER import widgets.** Use `NavigationService` for navigation. 6. **Pages use `Consumer` or `Provider.of` to read ViewModel state.** 7. **Colors from `AppColors`** — never hardcode hex in widgets. 8. **Text via string extensions** — `'Hello'.toText16()` not `Text('Hello')`. 9. **Spacing via int extensions** — `16.height` not `SizedBox(height: 16)`. 10. **Sizes via responsive extensions** — `16.f`, `16.w`, `16.h`, `16.r`. 11. **Assets via `AppAssets`** — never hardcode paths. 12. **Cache keys in `CacheConst`** — never raw strings for SharedPreferences. 13. **Endpoints in `ApiConsts`** — never hardcode URLs. 14. **Errors through `ErrorHandlerService`** — VMs never show dialogs. 15. **User-facing strings use `.tr()`** from easy_localization. --- ## 18. CHECKLIST — Verify Before Delivering - [ ] `flutter pub get` succeeds - [ ] All imports use `package:{{DART_PACKAGE}}/...` - [ ] Every file in this spec exists - [ ] `dependencies.dart` registers all existing services/repos/VMs - [ ] `main.dart` providers list includes all registered VMs - [ ] `app_routes.dart` includes all existing pages - [ ] `pubspec.yaml` declares all existing asset directories - [ ] Both `en-US.json` and `ar-SA.json` exist - [ ] No circular imports - [ ] `AppColors` used everywhere (no raw `Color()` in widgets) - [ ] Extensions used for text, spacing, widget wrapping --- *End of specification. Generate all files above. When asked to add a feature, use Section 16.*