alghwalbi_core_app 2.1.1
alghwalbi_core_app: ^2.1.1 copied to clipboard
alghwalbi_core_app package is primarily intended for personal use, but feel free to use it in your projects.
alghwalbi_core_app #
A production-ready Flutter foundation: networking, push notifications, device utilities, reusable UI, and a schema-driven dynamic form engine, all in one package.
Features • Installation • Quick start • Form Builder • API reference
Note
This package is primarily built for personal and internal projects, but you are welcome to use it in yours. The public API may evolve between minor versions; check the changelog before upgrading.
Table of contents #
- Features
- Architecture
- Requirements
- Installation
- Quick start
- Networking
- Push notifications
- Navigation
- Form Builder
- Utilities
- Widgets
- API reference
- Project structure
- License
Features #
Networking #
|
Form Builder #
|
Platform services #
|
UI toolkit #
|
Architecture #
The package ships as a single library (package:alghwalbi_core_app/alghwalbi_core_app.dart). Everything the host app needs is exported from that one import.
flowchart TB
App["Host Flutter app"]
subgraph Core["alghwalbi_core_app"]
direction TB
Router["AppRouter<br/><sub>navigation, dialogs</sub>"]
subgraph Services
Api["IApiService<br/><sub>DioApiService / HttpApiService</sub>"]
Push["PushNotificationService"]
Net["InternetConnectionService"]
end
subgraph FormBuilder["Form Builder"]
Models["Field models<br/><sub>JSON to typed fields</sub>"]
Engine["Engine<br/><sub>scope, visibility, calculation</sub>"]
Views["FormFieldWidget<br/><sub>per-type renderers</sub>"]
end
Utils["Utils<br/><sub>toast, date, location, camera, layout...</sub>"]
Widgets["Reusable widgets<br/><sub>buttons, dropdowns, app bar...</sub>"]
end
Backend[("Backend API")]
FCM[("Firebase Cloud Messaging")]
App --> Router & Api & Push & Views & Utils & Widgets
Models --> Engine --> Views
Api --> Backend
Push --> FCM
Design principles
| Principle | How it shows up |
|---|---|
| Host owns the infrastructure | The package never hardcodes your base URL, auth, or token storage. You provide them through IApiServiceHelpers, callbacks, and fetchers. |
| Typed results, not exceptions | Network calls return OperationResult<T> rather than throwing, so every call site handles success and failure explicitly. |
| Schema-driven UI | Forms are described by JSON and rendered by type; new forms need no new widget code. |
| Fail safe | Malformed visibility rules show the field rather than hide data; badge updates never crash the app. |
Requirements #
| Requirement | Version |
|---|---|
| Dart SDK | ^3.8.1 |
| Flutter | >= 1.17.0 (a recent stable channel is recommended) |
| Platforms | Android, iOS |
| Firebase | Required only if you use PushNotificationService |
Installation #
Add the package to your pubspec.yaml. Pinning to a tag or commit is recommended so upgrades are deliberate:
dependencies:
alghwalbi_core_app:
git:
url: https://github.com/ahmedabdelrahmanalghwalbi/alghwalbi_core_app.git
ref: master # or a specific tag / commit
Then fetch dependencies:
flutter pub get
And import the library:
import 'package:alghwalbi_core_app/alghwalbi_core_app.dart';
Platform setup checklist
Several features wrap platform plugins that need native configuration in the host app:
| Feature | Plugin | Host setup |
|---|---|---|
| Push notifications | firebase_messaging |
Add google-services.json / GoogleService-Info.plist, enable Push Notifications and Background Modes on iOS |
| Location | geolocator, geocoding |
Location usage descriptions in Info.plist, location permissions in AndroidManifest.xml |
| Camera / photos | image_picker, permission_handler |
NSCameraUsageDescription, NSPhotoLibraryUsageDescription |
| File uploads | file_picker |
Follow the plugin's platform notes |
| Localization | easy_localization |
Wrap the app in EasyLocalization if you use locale-aware widgets |
Quick start #
Wire the package's navigatorKey into your MaterialApp. Toasts, modal sheets, dialogs and navigation all resolve their context through it.
import 'package:flutter/material.dart';
import 'package:alghwalbi_core_app/alghwalbi_core_app.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: AppRouter.navigatorKey,
navigatorObservers: [AppRouter.routeObserver],
home: const HomePage(),
);
}
}
Anywhere in the app:
AppRouter.navigateTo(() => const DetailsPage(), animationType: AnimationTypes.slide);
ToastUtils.showSuccess('Saved successfully');
Networking #
Setting up DioApiService #
DioApiService delegates everything app-specific (headers, tokens, error mapping) to an IApiServiceHelpers implementation that you own.
sequenceDiagram
autonumber
participant UI as Screen
participant S as DioApiService
participant H as IApiServiceHelpers (yours)
participant C as Cache store
participant B as Backend
UI->>S: get<T>(url, dataKey, cacheType)
S->>H: buildHeadersMethod(addToken, contentType...)
H-->>S: headers
S->>C: lookup (per cacheType)
S->>B: HTTP request
B-->>S: response
alt 401 Unauthorized
S->>H: unauthrizedCallback(response)
end
S-->>UI: OperationResult<T>
final apiService = DioApiService(
dio: Dio(),
apiServiceHelper: MyApiServiceHelpers(), // implements IApiServiceHelpers
cancelTokenManager: DioApiCancelTokenManagerService(),
connectionTimeout: 30, // seconds
receiveTimeout: 30,
sendTimeout: 60,
enableCache: true,
);
Implementing IApiServiceHelpers
Your implementation decides how headers are built and how each failure category is turned into an OperationResult. Load tokens from your secure storage here; never hardcode them.
class MyApiServiceHelpers implements IApiServiceHelpers {
@override
void Function(dynamic response)? unauthrizedCallback = (_) {
// e.g. clear the session and route to login
};
@override
Future<Map<String, String>> buildHeadersMethod({
Map<String, dynamic>? additionalHeaders,
bool? addToken = true,
bool? addSessionCookie = false,
bool isBasicAuth = false,
String? basicUsername,
String? basicPassword,
RequestContentType contentType = RequestContentType.json,
RequestAcceptanceType acceptanceType = RequestAcceptanceType.json,
}) async {
final token = await secureStorage.read(key: 'access_token');
return {
'Accept': acceptanceType == RequestAcceptanceType.json ? 'application/json' : '*/*',
'Content-Type': contentType == RequestContentType.json
? 'application/json'
: 'application/x-www-form-urlencoded',
if (addToken == true && token != null) 'Authorization': 'Bearer $token',
...?additionalHeaders?.map((k, v) => MapEntry(k, '$v')),
};
}
@override
OperationResult<T> handleDIOBadResponse<T>({
required DioException error,
required String endpoint,
required StackTrace stackTrace,
}) => OperationResult<T>(
statusCode: error.response?.statusCode ?? 500,
message: 'Something went wrong. Please try again.',
);
// ...implement the remaining handle* methods the same way.
}
| Handler | Triggered when |
|---|---|
handleDIOConnectionError |
No internet, DNS failure, server unreachable |
handleDIOConnectionTimeout |
Server does not respond within connectTimeout |
handleDIOSendTimeout |
Upload / request body times out |
handleDIOReceiveTimeout |
Download / response body times out |
handleDIOBadCertificate |
TLS certificate validation fails |
handleDIOBadResponse |
Server responds with a 4xx / 5xx status |
handleDIOCancelledRequest |
Request cancelled (e.g. user left the page) |
handleDIOUnknownError |
Parsing, casting, or other unexpected errors |
handleDIOApiCallsErrorsMethod |
Any non-Dio exception during a call |
handleHTTP* |
Equivalents for HttpApiService |
Making requests #
Every method takes a currentPageKeyForCancelTokens (used to cancel in-flight requests when a page is disposed) and a dataKey.
final result = await apiService.get<Map<String, dynamic>>(
'$baseUrl/profile',
currentPageKeyForCancelTokens: 'ProfilePage',
dataKey: 'data',
addToken: true,
cacheType: RequestCacheTypes.refreshForceCache,
);
if (result.success) {
final profile = Profile.fromJson(result.data!);
} else {
ToastUtils.showError(result.message ?? 'Unable to load profile');
}
| Method | Purpose |
|---|---|
get |
Read data, with optional caching and image pre-caching |
post / put / patch / delete |
Standard JSON or URL-encoded writes |
postWithFormDataSingleFile |
Multipart upload of one File |
postWithFormDataMultipleFiles |
Multipart upload of a List<File> under one field |
postWithFormDataComplexFiles |
Multipart upload of a nested map of fields and files |
putWithFormDataSingleFile / putWithFormDataMultipleFiles / patchWithFormDataMultipleFiles |
Multipart updates |
downloadFile |
Stream a file to savePath with progress |
clearCache |
Wipe cached responses (call on logout) |
Caching strategies #
Caching is honored by DioApiService when constructed with enableCache: true. The strategy is chosen per request and never mutates global state.
RequestCacheTypes |
Reads cache | Hits network | Writes cache | Typical use |
|---|---|---|---|---|
request |
Depends on headers | Yes | Yes | Standard HTTP caching |
forceCache |
Yes | No | No | Full offline mode |
refresh |
No | Yes | Yes | Pull-to-refresh |
noCache |
No | Yes | No | Login, OTP, sensitive data |
refreshForceCache (default) |
On failure | Yes | Yes | Fresh online, fallback offline |
Important
Call apiService.clearCache() on logout so a new user on the same device never sees the previous user's cached responses.
Request cancellation #
@override
void dispose() {
apiService.cancelCurrentPageRequests(currentPageKeyForCancelTokens: 'ProfilePage');
super.dispose();
}
Use cancelAllRequests() to abort everything, for example on logout.
OperationResult<T> #
| Field | Type | Description |
|---|---|---|
success |
bool |
true for 2xx responses |
statusCode |
int |
HTTP status code |
bodyStatusCode |
int? |
Status code reported inside the response body |
data |
T? |
Parsed payload |
message / messageAr / messageUr |
String? |
Localized server messages |
hasNoInternetConnection |
bool? |
Set when the device was offline |
status |
ApiStatus? |
Optional structured status (code, message, token) |
responseHeaders |
Map<String, dynamic>? |
Raw response headers |
Push notifications #
PushNotificationService handles the Firebase Cloud Messaging lifecycle. You supply three callbacks; the service handles the rest.
flowchart LR
A["init()"] --> B{"Permission<br/>granted?"}
B -- No --> X["Log warning<br/>and stop"]
B -- Yes --> C["Configure foreground<br/>presentation"]
C --> D["Get FCM token"]
D --> E["registerDeviceTokenToServerMethod"]
E --> F["Listen for messages<br/>and token refresh"]
F --> G["onReceiveNotification"]
final pushService = PushNotificationService(
registerDeviceTokenToServerMethod: ({required notificationDeviceToken}) async {
await myApi.registerDevice(notificationDeviceToken);
},
unregisterDeviceTokenToServerMethod: () async => myApi.unregisterDevice(),
onReceiveNotification: ({required message}) async {
// Route the user or refresh data based on message?.data
},
);
await Firebase.initializeApp();
await pushService.init();
| Method | Description |
|---|---|
init() |
Request permission, configure presentation, register the token, start listeners |
unRegisterNotificaitonDeviceToken() |
Remove the token from your backend and FCM (call on logout) |
getNotificationPermissionStatus() |
Whether notifications are currently authorized |
Badge control is available through NotificationBadgeUtils:
await NotificationBadgeUtils.setNotificationBadgeCount(3);
await NotificationBadgeUtils.clearNotificationBadge(isIOSOnly: true);
Navigation #
AppRouter is a static, context-free router built on AppRouter.navigatorKey.
AppRouter.navigateTo(() => const HomePage(), replaceAll: true, animationType: AnimationTypes.fade);
AppRouter.goBack(result: selectedItem);
AppRouter.goBackUntil(routeName: '/');
final confirmed = await AppRouter.showConfirmMessageWithLogo(
// ...title, message, icon
confirmText: 'Delete',
denyText: 'Cancel',
);
| API | Description |
|---|---|
navigateTo(creator, replace, replaceAll, animationType) |
Push, replace, or reset the stack with none, fade or slide transitions |
goBack<T>({result}) / goBackUntil(routeName) |
Pop safely |
showConfirmMessageWithLogo(...) |
Confirmation dialog with an SVG or icon header; returns bool |
showConfirmDialogWithOptionalTextField(...) |
Confirmation dialog that can collect a text input |
subscribeRouteObserver / unsubscribeRouteObserver |
Hook a RouteAware into AppRouter.routeObserver |
Form Builder #
The Form Builder turns a JSON form definition into a fully interactive Flutter form. Each field is parsed into a typed model, the engine resolves visibility and calculations within the correct scope, and FormFieldWidget renders it.
flowchart LR
J["JSON schema"] -->|"FormFieldModel.getFormFieldByType"| M["Typed field models"]
M --> C{"FormSchemaCapabilities.detect"}
C -- "static form" --> R["Flat renderer"]
C -- "rules, groups or calculations" --> E["Dynamic engine"]
E --> V["FormVisibilityUtils<br/><sub>evaluate, prune</sub>"]
E --> K["FormCalculationUtils<br/><sub>derive, compute, aggregate</sub>"]
V & K --> W["FormFieldWidget"]
R --> W
W -->|"onChanged"| E
FormSchemaCapabilities.detect(fields).usesDynamicEngine tells you whether a form needs the scope-aware engine. Forms that use none of the dynamic features render exactly as before, with no configuration.
Supported field types #
The field type comes from the name property of each field in the schema. Several spellings are accepted (for example radio_group, radio-group, radiogroup).
| Category | Type | Schema name | Model |
|---|---|---|---|
| Input | Text | input | TextFormFieldModel |
| Text area | text_area | TextAreaFormFieldModel | |
| Number | numberinput | NumberFormFieldModel | |
| Checkbox | checkbox | CheckboxFormFieldModel | |
| Toggle | toggle | ToggleFormFieldModel | |
| Radio group | radio_group | RadioGroupFormFieldModel | |
| Dropdown | dropdown | DropdownFormFieldModel | |
| Dynamic dropdown | dynamic_dropdown or dropdown with entityConfig | DynamicDropdownFormFieldModel | |
| Multi select | multi_select | MultiSelectFormFieldModel | |
| Dynamic multi select | dynamic_multi_select or multi_select with entityConfig | DynamicMultiSelectFormFieldModel | |
| Date | datepicker | DatePickerFormFieldModel | |
| Date and time | datetime | DateTimePickerFormFieldModel | |
| File uploader | uploader | FileUploaderFormFieldModel | |
| Take photo | camera | TakePhotoFormFieldModel | |
| Multiple photos | FormFieldType.multipleTakePhotos (no schema alias yet) | MultipleTakePhotosFormFieldModel | |
| Signature | signature | SignatureFormFieldModel | |
| Calculated | Derived value | derived_value | DerivedValueFormFieldModel |
| Computed value | computed_value | ComputedValueFormFieldModel | |
| Static | Header | header | HeaderFormFieldModel |
| Label | label | LabelFormFieldModel | |
| Image | image | ImageFormFieldModel | |
| Link | link | LinkFormFieldModel | |
| Progress circle | progress_circle | ProgressCircleFormFieldModel | |
| Progress line | progress_line | ProgressLineFormFieldModel | |
| Structure | Container | container | ContainerFormFieldModel |
| Repeating group | repeating_group | RepeatingGroupFormFieldModel |
Every field shares these base properties from FormFieldModel:
{
"key": "customer_name",
"name": "input",
"title": "Customer name",
"placeholder": "Enter the customer name",
"required": { "value": true, "validationMessage": "Customer name is required" },
"requiredWhenVisible": false,
"visibilityRules": null,
"css": {}
}
Rendering a form #
final fields = (schema['fields'] as List)
.map((json) => FormFieldModel.getFormFieldByType(json))
.toList();
final scope = FormFieldScopeUtils.flatten(fields);
ListView(
children: FormVisibilityUtils.visibleFields(fields, scope)
.map((field) => FormFieldWidget(
fieldModel: field,
scopeFields: scope,
primaryColor: Theme.of(context).primaryColor,
locale: context.locale,
spacing: 16,
innerSpacing: 8,
fieldRadius: 12,
fieldFillColor: Colors.white,
fieldBorderSize: 1,
fieldBorderColor: Colors.grey.shade300,
fieldHeight: 52,
fieldIconSize: 20,
fieldLabelTextStyle: textTheme.titleSmall!,
fieldTextStyle: textTheme.bodyMedium!,
fieldHintStyle: textTheme.bodyMedium!.copyWith(color: Colors.grey),
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
onChanged: (key, value) {
FormVisibilityUtils.pruneForm(fields);
FormCalculationUtils.recalculateForm(fields);
setState(() {});
},
))
.toList(),
);
FormFieldWidget also exposes per-type customization, including labels for date pickers, photo sources, signature pads, uploaders and repeating groups, so every visible string can be localized by the host.
Dynamic dropdowns #
Dropdowns arriving with data.queryKey: "dynamic" and a data.entityConfig are parsed as DynamicDropdownFormFieldModel. Their options come from the backend entity endpoint (GET /api/v1/form-entities/{entityKey}/data) rather than the form definition.
The package does not perform the HTTP call. The host supplies a DynamicDropdownOptionsFetcher, so authentication and base URL stay under the app's control:
FormFieldWidget(
// ...existing parameters
dynamicDropdownOptionsFetcher: (request) async {
final response = await apiClient.get(
'/api/v1/form-entities/${request.entityKey}/data',
queryParameters: request.toQueryParameters(),
);
return DynamicDropdownOptionsPageModel.fromJson(response.data);
},
// Required for dependent dropdowns: return the current value of the parent field.
dynamicDropdownParentValueResolver: (parentFieldKey) => formValues[parentFieldKey],
dynamicDropdownSearchHintText: 'Search',
dynamicDropdownRetryButtonText: 'Retry',
dynamicDropdownNoResultsText: 'No results',
dynamicDropdownLoadFailedText: 'Failed to load options',
)
sequenceDiagram
autonumber
actor U as User
participant P as Parent dropdown
participant C as Child dropdown
participant F as Your fetcher
participant API as Backend
U->>P: Select "Region A"
P-->>C: Parent value changed, clear selection, enable
U->>C: Open and type "cai"
C->>F: request(entityKey, search, offset=0, parentValue)
F->>API: GET /form-entities/{entityKey}/data
API-->>F: { items, hasMore: true }
F-->>C: DynamicDropdownOptionsPageModel
U->>C: Scroll to end
C->>F: request(offset=50)
Behavior
- Options load when the field opens, with debounced server-side search and infinite-scroll pagination driven by
hasMore. - The stored value (and the one emitted through
onChanged) is{ "id": ..., "displayValue": ..., "entityKey": ... }, the shape the submissions endpoint expects. - A dropdown whose
entityConfig.dependsOnis set stays disabled until its parent has a value, and is cleared whenever the parent changes. The parent id is sent asparentValuealong withparentEntityKey. - If the fetcher throws or returns
null, the field shows a retry state. Without a fetcher, dynamic dropdowns are skipped with a warning log.
DynamicDropdownOptionsRequestModel |
Description |
|---|---|
entityKey |
Entity to query |
displayField |
Property to show as the option label |
limit / offset |
Paging (default 50 / 0) |
search |
Current search text |
parentEntityKey / parentValue |
Parent entity and its selected id, for dependent dropdowns |
includeProperties |
Extra properties returned with every option, so derived fields can read them offline |
Visibility rules #
Any field can declare when it should be shown. Conditions always refer to sibling fields in the same scope: top-level fields read top-level values, and fields inside a repeating-group item read only that item.
{
"key": "rejection_reason",
"name": "text_area",
"title": "Reason for rejection",
"requiredWhenVisible": true,
"visibilityRules": {
"operator": "AND",
"conditions": [
{ "field": "decision", "operator": "equals", "value": "rejected" },
{ "field": "category", "operator": "in", "value": ["safety", "quality"] }
]
}
}
| Operator | Aliases | Meaning |
|---|---|---|
equals |
eq, == |
Value matches |
notEquals |
neq, != |
Value differs |
in |
Value is one of the list | |
notIn |
not_in |
Value is none of the list |
exists |
Field holds a meaningful answer | |
notExists |
not_exists |
Field is unanswered |
- Combinators: the rule-level
operatorisAND(&&, the default) orOR(||). - Shorthand and nesting: a single
showWhencondition is accepted, and an entry ofconditionsthat has its ownconditionsis treated as a nested group. - Type tolerant:
49,49.0and"49"compare as equal. - Meaningful values:
null,false,0, blank text and empty lists or groups count as unanswered forexists. - Safe by default: an incomplete rule, or one pointing outside the scope, keeps the field visible so no data is hidden by mistake.
- Pruning:
FormVisibilityUtils.pruneForm(fields)clears the values of hidden fields, following chains such as action → reason → reason details until nothing else changes. - A hidden field is never required, whatever
requiredorrequiredWhenVisiblesays.
Calculated fields #
Calculated fields are read-only and resolved by FormCalculationUtils. They give the user immediate feedback; the server recomputes them on submission and its result is authoritative.
| Kind | Config key | How the value is obtained |
|---|---|---|
| Derived, lookup | derivedConfig.sourceField + sourceProperty |
Reads a property of the entity selected in another field, optionally combined with a second field through calculation (for example unit price × quantity) |
| Derived, formula | derivedConfig.operands + operator |
Combines other fields in the same scope |
| Derived, aggregate | derivedConfig.groupField + childField + operator |
Reduces one child field across every item of a repeating group |
| Computed | computedConfig.operands + operator |
Combines fields in the field's own scope |
Operators: add, subtract, multiply, divide, sum, product, min, max, avg (with aliases such as +, -, *, /, average, mean).
Chains like subtotal → additional → total settle in a single recalculateForm call, and aggregates and top-level formulas are resolved together so a formula can read an aggregate (for example the total of every item plus a call-out fee). A cycle in the definition is detected and logged rather than looping forever.
Repeating groups #
A repeating group lets the user add, edit and remove items built from the template in its fields array. Each item owns its own field instances, so values, dependent dropdown options and visibility rules are independent between items.
{
"key": "materials",
"name": "repeating_group",
"title": "Materials used",
"groupConfig": {
"itemLabel": "Material",
"addItemLabel": "Add material",
"minItems": 1,
"maxItems": 10,
"allowDelete": true,
"allowEdit": true,
"collapsibleItems": true,
"collapseCompletedItems": false
},
"fields": [
{ "key": "material", "name": "dropdown", "title": "Material" },
{ "key": "quantity", "name": "numberinput", "title": "Quantity" }
]
}
The host controls item lifecycle through these FormFieldWidget callbacks:
| Callback | Purpose |
|---|---|
onRepeatingGroupAddItem(groupKey) |
Append a new item |
onRepeatingGroupDeleteItem(groupKey, index) |
Remove an item (confirm first if it holds data) |
onRepeatingGroupItemExpandedChanged(groupKey, index, expanded) |
Track collapse state |
onRepeatingGroupItemFieldChanged(groupKey, index, fieldKey, value) |
Prune, recalculate and persist the item's scope |
Utilities #
All utilities are static and require no setup beyond the quick start.
| Utility | Highlights |
|---|---|
ToastUtils |
showSuccess, showError, showWarning, showInfo with auto-close |
DateTimeUtils |
Locale-aware formatting (formatDate, formatReadableDate, formatTime12Hour, formatRelativeShort, ...), API formats, adaptive date picker, date-range modal sheet |
ModalSheetUtils |
showCustomModalSheet<T> with title, drag handle and blurred backdrop |
LayoutUtils |
isMobile / isTablet / isDesktop, bindWidget for responsive builders, screen sizes |
LocationUtils |
getCurrentLocation with guided permission and service dialogs |
LocationPermissionHandlerUtils |
checkLocationRequirements before tracking |
AddressUtils |
getAddressFromCoordinates with provider fallback |
CameraUtils |
takeSelfie with camera permission handling |
InfoUtils |
getDeviceId, getEnhancedDeviceId, getDeviceInfo, getAppVersion |
FormatePhoneNumberUtils |
formatPhoneNumberForServer using an IsoCode |
NotificationBadgeUtils |
Set or clear the app icon badge, optionally iOS only |
FalvorUtils |
getFlavor() returns Flavor.dev, stg or prod from --flavor (or --dart-define=FLAVOR on web) |
StringUtils |
getInitials, buildHyphenatedString |
ColorsUtils |
getRandomColor |
AppBaseModel |
Null-safe JSON parsing: parseInt, parseDouble, parseString, parseBool, parseDateTime, parseList<T>, parseMap, ... |
AppImageCacheManagerUtils |
Shared CacheManager for network images |
PackageLogger |
debug, info, warning, error logging |
InternetConnectionService |
hasConnection and an onStatusChange stream |
final isOnline = await InternetConnectionService().hasConnection;
final label = DateTimeUtils.formatReadableDate(dateTime: DateTime.now(), locale: 'ar');
LayoutUtils.bindWidget(
context,
mobileWidget: () => const MobileLayout(),
tabletWidget: () => const TabletLayout(),
);
Widgets #
| Widget | Description |
|---|---|
CustomElevatedButton |
Async-aware button with built-in loading state, gradients and disabled styling |
CustomActionButtonWidget |
Primary / secondary action button with optional gradient |
GradientFloatingActionButtonWidget |
FAB with gradient fill and stroke |
GeneralAppbarWidget |
Consistent app bar with optional back button and actions |
CustomTextFieldColumnWidget |
Labeled text field with validation and password mode |
SearchBoxWidget |
Search input with shimmer loading state |
SelectOptionWidget / MultiSelectOptionWidget |
Labeled single and multi select pickers |
CustomDropdown |
Animated, searchable single-select dropdown |
DynamicCustomDropdown / DynamicCustomMultiDropdown |
Paged, server-driven dropdowns |
PlaceholderWidget |
Empty / error state with retry button |
CustomAnimatedHeroPopupWidget |
Hero-animated popup that expands from its trigger |
LanguageSwitcherWidget |
English / Arabic / Urdu switcher |
OrientationInitializerWidget |
Locks orientation by device class (phone vs tablet) |
CustomDividerWidget |
Themed divider |
CustomElevatedButton(
title: 'Submit',
backgroundColor: Theme.of(context).primaryColor,
onPressed: () async => await submitForm(),
);
API reference #
Services
| Type | Kind |
|---|---|
IApiService |
Abstract contract for HTTP clients |
DioApiService |
Dio implementation with caching, cancellation and upload progress |
HttpApiService |
package:http implementation (no caching) |
IApiServiceHelpers |
Host-implemented headers, auth and error mapping |
DioApiCancelTokenManagerService |
Per-page cancel token registry |
ApiLogger |
Request / response / error logging |
PushNotificationService |
FCM lifecycle |
InternetConnectionService |
Connectivity checks |
OperationResult<T>, ApiStatus |
Result types |
RequestContentType, RequestAcceptanceType, RequestCacheTypes |
Request enums |
Form Builder
| Type | Kind |
|---|---|
FormFieldModel |
Base model and getFormFieldByType factory |
InputFormFieldModel, StaticFormFieldModel, StructureFormFieldModel |
Field categories |
FormFieldType, CalculationOperator |
Enums |
VisibilityRulesModel, VisibilityConditionModel, VisibilityOperator, VisibilityCombinator |
Visibility rules |
RepeatingGroupConfigModel, RepeatingGroupItemModel |
Repeating groups |
DynamicDropdownOptionsFetcher, DynamicDropdownOptionsRequestModel, DynamicDropdownOptionsPageModel |
Dynamic options contract |
FormSchemaCapabilities |
Detects which dynamic features a schema uses |
FormFieldScopeUtils |
flatten, fieldByKey, valueByKey, setValueByKey, groupsOf |
FormVisibilityUtils |
isVisible, visibleFields, pruneHiddenValues, pruneForm |
FormCalculationUtils |
recalculateForm, recalculateScope, recalculateAggregates, apply, formatValue |
FormFieldWidget |
Type-dispatching renderer |
Assets
PackageAssetsConstants exposes the bundled SVGs (calendar, upload, repeating-group edit / delete / empty-state icons, and no_data.svg).
Project structure #
lib/
├── alghwalbi_core_app.dart # Single library entry point
├── router/
│ └── app.router.dart # AppRouter, transitions, dialogs
└── core/
├── constants/ # Package asset paths
├── models/ # AppBaseModel, OperationResult
├── services/
│ ├── api/ # IApiService, Dio & http clients, logging
│ ├── internet_connection/
│ └── push_notifications/
├── utils/ # Toast, date, layout, location, camera...
├── widgets/ # Buttons, app bar, dropdowns, popups...
│ └── custom_dropdown/ # Single, multi and dynamic dropdowns
└── form_builder/
├── models/ # Inputs, statics, structure, enums, rules
├── engine/ # Scope, visibility, calculation
└── views/ # Per-type field widgets
License #
Released under the MIT License.