You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HMG_Patient_App_New/PROJECT_ARCHITECTURE.md

62 KiB

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:

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.

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

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    # prefer_single_quotes: true

5. FOLDER STRUCTURE — Create ALL These Directories

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

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

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<Object?> get props => [message];
}

class DataParsingFailure extends Failure {
  const DataParsingFailure(super.message);
  @override
  List<Object?> get props => [message];
}

class ConnectivityFailure extends Failure {
  const ConnectivityFailure(super.message);
  @override
  List<Object?> get props => [message];
}

class UnAuthenticatedUserFailure extends Failure {
  final String url;
  const UnAuthenticatedUserFailure(super.message, {this.url = ""});
  @override
  List<Object?> get props => [message];
}

class AppUpdateFailure extends Failure {
  const AppUpdateFailure(super.message);
  @override
  List<Object?> get props => [message];
}

class StatusCodeFailure extends Failure {
  const StatusCodeFailure(super.message);
  @override
  List<Object?> get props => [message];
}

class UnknownFailure extends Failure {
  final String url;
  const UnknownFailure(super.message, {this.url = ""});
  @override
  List<Object?> get props => [message];
}

class UserIntimationFailure extends Failure {
  const UserIntimationFailure(super.message);
  @override
  List<Object?> get props => [message];
}

class MessageStatusFailure extends Failure {
  const MessageStatusFailure(super.message);
  @override
  List<Object?> get props => [message];
}

class InvalidCredentials extends Failure {
  const InvalidCredentials(String? message) : super(message ?? '');
  @override
  List<Object?> get props => [message];
}

class LocalStorageFailure extends Failure {
  const LocalStorageFailure(super.message);
  @override
  List<Object?> get props => [message];
}

6.3 FILE: lib/core/common_models/generic_api_model.dart

class GenericApiModel<T> {
  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<String, dynamic> json,
    T Function(Object? json)? fromJsonT,
  ) {
    return GenericApiModel<T>(
      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<String, dynamic> 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

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

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

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<String, dynamic>? authenticatedUser;

  void setAuthenticatedUser(Map<String, dynamic>? user) {
    authenticatedUser = user;
    isAuthenticated = user != null;
  }

  void resetLocation() {
    userLat = 0.0;
    userLong = 0.0;
  }
}

6.7 FILE: lib/core/app_assets.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

export '../routes/app_routes.dart';
export 'utils/size_utils.dart';

6.9 FILE: lib/core/utils/size_utils.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

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<NavigationService>().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

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<void> post(
    String endPoint, {
    required Map<String, dynamic> 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<String, String>? apiHeaders,
  });

  Future<void> get(
    String endPoint, {
    required Function(dynamic response, int statusCode) onSuccess,
    required Function(String error, int statusCode) onFailure,
    Map<String, dynamic>? queryParams,
    Map<String, String>? apiHeaders,
    bool isExternal = false,
  });
}

class ApiClientImp implements ApiClient {
  final AppState _appState;

  ApiClientImp({required AppState appState}) : _appState = appState;

  Map<String, String> _defaultHeaders() => {'Content-Type': 'application/json', 'Accept': 'application/json'};

  void _injectCommonFields(Map<String, dynamic> 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<void> post(
    String endPoint, {
    required Map<String, dynamic> 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<String, String>? 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<void> get(
    String endPoint, {
    required Function(dynamic response, int statusCode) onSuccess,
    required Function(String error, int statusCode) onFailure,
    Map<String, dynamic>? queryParams,
    Map<String, String>? 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

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

import 'package:flutter/material.dart';

class NavigationService {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  BuildContext? get context => navigatorKey.currentContext;

  Future<T?> push<T>(Route<T> route) => navigatorKey.currentState!.push(route);

  Future<T?> pushAndRemoveUntil<T>(Route<T> route, RoutePredicate predicate) =>
      navigatorKey.currentState!.pushAndRemoveUntil(route, predicate);

  void pop<T extends Object?>([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<T?> pushPage<T>({required Widget page, bool fullscreenDialog = false}) =>
      navigatorKey.currentState!.push<T>(
        MaterialPageRoute(builder: (_) => page, fullscreenDialog: fullscreenDialog),
      );
}

7.3 FILE: lib/services/cache_service.dart

import 'dart:convert';
import 'package:{{DART_PACKAGE}}/services/logger_service.dart';
import 'package:shared_preferences/shared_preferences.dart';

abstract class CacheService {
  Future<void> saveString({required String key, required String value});
  Future<void> saveInt({required String key, required int value});
  Future<void> saveBool({required String key, required bool value});
  String? getString({required String key});
  int? getInt({required String key});
  bool? getBool({required String key});
  Future<dynamic> getObject({required String key});
  Future<void> saveObject({required String key, required dynamic value});
  Future<void> remove({required String key});
  Future<void> clear();
}

class CacheServiceImp implements CacheService {
  final SharedPreferences sharedPreferences;
  final LoggerService loggerService;

  CacheServiceImp({required this.sharedPreferences, required this.loggerService});

  @override
  Future<void> saveString({required String key, required String value}) async => await sharedPreferences.setString(key, value);
  @override
  Future<void> saveInt({required String key, required int value}) async => await sharedPreferences.setInt(key, value);
  @override
  Future<void> 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<dynamic> 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<void> saveObject({required String key, required dynamic value}) async =>
      await sharedPreferences.setString(key, json.encode(value));
  @override
  Future<void> remove({required String key}) async => await sharedPreferences.remove(key);
  @override
  Future<void> clear() async => await sharedPreferences.clear();
}

7.4 FILE: lib/services/dialog_service.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<void> showErrorBottomSheet({String title, required String message, Function()? onOkPressed});
  Future<void> 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<void> 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<void> 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

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<void> 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<void> 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<void> _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

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<String?> 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<String> 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

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<void> initialize({Function(String payload)? onNotificationClick});
  Future<void> showNotification({required String title, required String body, String? payload});
  Future<void> cancelAllNotifications();
}

class NotificationServiceImp implements NotificationService {
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
  final LoggerService loggerService;

  NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService});

  @override
  Future<void> 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<void> 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<void> cancelAllNotifications() async => await flutterLocalNotificationsPlugin.cancelAll();
}

7.8 FILE: lib/services/analytics/analytics_service.dart

import 'package:firebase_analytics/firebase_analytics.dart';

class GAnalytics {
  final _analytics = FirebaseAnalytics.instance;

  Future<void> logEvent(String name, {Map<String, dynamic>? parameters}) async {
    try {
      await _analytics.logEvent(name: name.trim().toLowerCase(), parameters: parameters?.map((k, v) => MapEntry(k, v as Object)));
    } catch (_) {}
  }

  Future<void> 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

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

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

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

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

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

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<T?> showSheet<T>({required Widget child, bool isScrollControlled = true, bool isDismissible = true}) {
    return showModalBottomSheet<T>(context: this, isScrollControlled: isScrollControlled, isDismissible: isDismissible, backgroundColor: Colors.transparent, builder: (_) => child);
  }
}

9.5 FILE: lib/extensions/route_extensions.dart

import 'package:flutter/material.dart';

extension NavigationExtensions on BuildContext {
  void navigateWithName(String routeName, {Object? arguments}) => Navigator.pushNamed(this, routeName, arguments: arguments);
  Future<void> 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

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

import 'package:flutter/material.dart';

class FadePage<T> extends PageRouteBuilder<T> {
  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

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<void> addDependencies() async {
    final logger = Logger(printer: PrettyPrinter(methodCount: 2, errorMethodCount: 5, lineLength: 1000, colors: true, printEmojis: true));

    getIt.registerLazySingleton<LoggerService>(() => LoggerServiceImp(logger: logger));
    getIt.registerLazySingleton<NavigationService>(() => NavigationService());
    getIt.registerLazySingleton<AppState>(() => AppState(navigationService: getIt()));
    getIt.registerLazySingleton<GAnalytics>(() => GAnalytics());
    getIt.registerLazySingleton<DialogService>(() => DialogServiceImp(navigationService: getIt()));
    getIt.registerLazySingleton<ErrorHandlerService>(() => ErrorHandlerServiceImp(dialogService: getIt(), loggerService: getIt(), navigationService: getIt()));

    final sp = await SharedPreferences.getInstance();
    getIt.registerLazySingleton<CacheService>(() => CacheServiceImp(sharedPreferences: sp, loggerService: getIt()));
    getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt()));
    getIt.registerLazySingleton<FirebaseService>(() => FirebaseServiceImpl(firebaseMessaging: FirebaseMessaging.instance, loggerService: getIt(), appState: getIt()));

    final flnp = FlutterLocalNotificationsPlugin();
    getIt.registerLazySingleton<NotificationService>(() => NotificationServiceImp(flutterLocalNotificationsPlugin: flnp, loggerService: getIt()));

    // ═══ Register feature repos & ViewModels below as you add them ═══
  }
}

12. ROUTING — lib/routes/app_routes.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<String, WidgetBuilder> get routes => {
    initialRoute: (_) => const SplashPage(),
    landingScreen: (_) => const LandingNavigation(),
  };
}

13. LOCALIZATION FILES

FILE: assets/langs/en-US.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

{
  "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

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<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    ApiConsts.setBackendURLs();
    await getIt.get<NotificationService>().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

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<LandingNavigation> createState() => _LandingNavigationState();
}

class _LandingNavigationState extends State<LandingNavigation> {
  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

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

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<void> _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<NavigationService>().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

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<Either<Failure, GenericApiModel<List<YourModel>>>> getItems();
}

class {{Feature}}RepoImp implements {{Feature}}Repo {
  final ApiClient apiClient;
  final LoggerService loggerService;
  {{Feature}}RepoImp({required this.loggerService, required this.apiClient});

  // @override
  // Future<Either<Failure, GenericApiModel<List<YourModel>>>> getItems() async {
  //   Map<String, dynamic> body = {};
  //   try {
  //     GenericApiModel<List<YourModel>>? 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

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

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:

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:

ChangeNotifierProvider<{{Feature}}ViewModel>(create: (_) => getIt.get<{{Feature}}ViewModel>()),

app_routes.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<T>() elsewhere.
  3. All repos return Future<Either<Failure, GenericApiModel<T>>>.
  4. All ViewModels extend ChangeNotifier. Call notifyListeners() after state changes.
  5. ViewModels NEVER import widgets. Use NavigationService for navigation.
  6. Pages use Consumer<T> or Provider.of<T> 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 extensions16.height not SizedBox(height: 16).
  10. Sizes via responsive extensions16.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.