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

1769 lines
62 KiB
Markdown

<SYSTEM>
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.
</SYSTEM>
# 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<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`
```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`
```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<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`
```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<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`
```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`
```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<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`
```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`
```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`
```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`
```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`
```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`
```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`
```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<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`
```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`
```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<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`
```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`
```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`
```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<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`
```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`
```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<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`
```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`
```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<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 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.*