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.
1769 lines
62 KiB
Markdown
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.*
|
|
|