62 KiB
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
- Every service/repo = abstract class + implementation. Never use concrete directly.
- All DI in
dependencies.dart. Only usegetIt.get<T>()elsewhere. - All repos return
Future<Either<Failure, GenericApiModel<T>>>. - All ViewModels extend
ChangeNotifier. CallnotifyListeners()after state changes. - ViewModels NEVER import widgets. Use
NavigationServicefor navigation. - Pages use
Consumer<T>orProvider.of<T>to read ViewModel state. - Colors from
AppColors— never hardcode hex in widgets. - Text via string extensions —
'Hello'.toText16()notText('Hello'). - Spacing via int extensions —
16.heightnotSizedBox(height: 16). - Sizes via responsive extensions —
16.f,16.w,16.h,16.r. - Assets via
AppAssets— never hardcode paths. - Cache keys in
CacheConst— never raw strings for SharedPreferences. - Endpoints in
ApiConsts— never hardcode URLs. - Errors through
ErrorHandlerService— VMs never show dialogs. - User-facing strings use
.tr()from easy_localization.
18. CHECKLIST — Verify Before Delivering
flutter pub getsucceeds- All imports use
package:{{DART_PACKAGE}}/... - Every file in this spec exists
dependencies.dartregisters all existing services/repos/VMsmain.dartproviders list includes all registered VMsapp_routes.dartincludes all existing pagespubspec.yamldeclares all existing asset directories- Both
en-US.jsonandar-SA.jsonexist - No circular imports
AppColorsused everywhere (no rawColor()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.