PLEX: Enterprise-Grade Flutter UI Framework
| Abdur Rahman GitHub |
PLEX is a powerful, open-source UI framework for Flutter, designed to accelerate the development of enterprise applications. With a focus on scalability, maintainability, and developer productivity, PLEX provides a robust foundation, modern UI components, and best practices for building high-quality business apps.
๐ Table of Contents
- Features
- Widgets & Components โ Data Tables, Charts, Dashboard, Timeline, Calendar, Rich Text, Backgrounds, Forms
- Layout & Navigation โ Navigation Rail, Cards, Backgrounds, Routing, Guards
- Loading & Feedback
- State Management
- Utilities
- Networking & API
- Data Storage โ PlexSp, PlexDb, Query Builder, Migrations, Relations, Encryption
- Dependency Injection
- Getting Started
- Usage Examples
โจ Features
- Enterprise Boilerplate: Rapidly scaffold production-ready apps with built-in routing, theming, and screen management.
- Navigation Modernization (v1.5.x): Pluggable router (GetX or GoRouter), route guards, parameterized paths, and deep linking.
- UI & Widget Expansion (v1.7.x): Charts (bar, line, pie, donut, sparkline), dashboard cards, timeline, calendar, rich text editor, form improvements (conditional fields, validator, wizard, stepper, color, file), and unified data table API.
- Modern UI Widgets: Rich set of customizable widgetsโadvanced tables, forms, loaders, shimmers, and more.
- MVVM Architecture: Built-in support for Model-View-ViewModel with
PlexAsyncActionfor zero-boilerplate async operations. - Dependency Injection: Tag-based DI with scoped lifetimes, circular dependency detection, async lazy init, and
PlexDisposablecleanup. - User Management: Integrated login screens, session handling, and user models.
- SignalR Integration: Native support for real-time communication using Microsoft SignalR.
- Persistent Storage: Easy-to-use local storage (PlexSp, PlexDb/Sembast) for app data and user preferences.
- Database Enhancements (v1.8.x): Fluent query builder, relations (hasMany/belongsTo), migrations, reactive streams, and optional AES-256 encryption at rest.
- Localization & Accessibility (v1.9.x): Built-in i18n via
PlexStringsoverridable string catalog,PlexLocalizationConfig,context.plexStrings,PlexAccessibilityConfig(high contrast, large text, reduce motion), and systematicSemanticsannotations across form fields, tables, navigation, and highlight widgets. - Networking: Full-featured HTTP client with interceptors, timeouts, cancellation, response caching, and type-safe parsing.
- Structured Logging:
PlexLoggerwith configurable levels, release-mode suppression, and optional remote sinks. - Material 2 & 3, Light & Dark Modes: Effortlessly switch between Material versions and color schemes.
- Code Generation: Annotation-based model enhancements (e.g.,
copy(),asString()methods). - Extensible & Customizable: Designed for flexibilityโoverride, extend, and adapt to your needs.
๐งฉ Widgets & Components
PLEX offers a comprehensive suite of widgets and utilities for enterprise Flutter development. Below are the most important components, with usage examples:
Data Tables
PlexDataTable
A powerful, customizable data table with sorting, searching, and export features.
PlexDataTable(
columns: [PlexDataCell.text("ID"), PlexDataCell.text("Name")],
rows: [
[PlexDataCell.text("1"), PlexDataCell.text("Alice")],
[PlexDataCell.text("2"), PlexDataCell.text("Bob")],
],
enableSearch: true,
enablePrint: true,
)
PlexAdvDataTable
A modern, feature-rich data table with advanced export (Excel, PDF) and pagination.
PlexAdvDataTable(
columns: [PlexDataCell.text("ID"), PlexDataCell.text("Name")],
rows: [
[PlexDataCell.text("1"), PlexDataCell.text("Alice")],
[PlexDataCell.text("2"), PlexDataCell.text("Bob")],
],
enableExportExcel: true,
enableExportPdf: true,
)
PlexDataTableWithPages
Paginated data table for large datasets.
PlexDataTableWithPages(
columns: [PlexDataCell.text("ID"), PlexDataCell.text("Name")],
rows: [...],
)
PlexDataTableUnified (v1.7.x)
Unified API for list, paginated, and stream-based data sources. Delegates to existing table widgets.
// In-memory list
PlexDataTableUnified(
source: PlexTableSource.list(rows),
columns: [PlexDataCell.text("ID"), PlexDataCell.text("Name")],
features: PlexTableFeatures(search: true, print: true),
)
// Paginated (async fetch per page)
PlexDataTableUnified(
source: PlexTableSource.paginated((page) => api.fetchPage(page)),
columns: [...],
)
// Stream (reactive updates)
PlexDataTableUnified(
source: PlexTableSource.stream(dataStream),
columns: [...],
)
| PlexTableSource | Description |
|---|---|
PlexTableSource.list(rows) |
In-memory list of rows |
PlexTableSource.paginated(fetchFn) |
Async fetch per page |
PlexTableSource.stream(stream) |
Reactive stream of row lists |
| PlexTableFeatures | Description |
|---|---|
search |
Enable search |
export |
List of PlexExportFormat (xlsx, pdf) |
groupBy |
Enable grouping |
editing |
Enable cell editing |
print |
Enable print |
| PlexExportFormat | Description |
|---|---|
PlexExportFormat.xlsx |
Export to Excel |
PlexExportFormat.pdf |
Export to PDF |
Charts & Data Visualization (v1.7.x)
All chart widgets use syncfusion_flutter_charts with theme-aware defaults.
PlexBarChart
Vertical or horizontal bar chart with optional stacking. Built on syncfusion_flutter_charts.
PlexBarChart(
title: 'Revenue',
series: [
PlexBarSeries(name: 'Q1', data: [('Jan', 1200.0), ('Feb', 980.0)], color: Colors.blue),
PlexBarSeries(name: 'Q2', data: [('Jan', 1400.0), ('Feb', 1100.0)], color: Colors.green),
],
orientation: PlexBarOrientation.vertical,
stacked: false,
)
| PlexBarSeries | Description |
|---|---|
name |
Series label |
data |
List<(String, double)> โ (category, value) pairs |
color |
Bar color |
PlexLineChart
Line chart with optional area fill. Uses SplineSeries / SplineAreaSeries.
PlexLineChart(
title: 'Users',
series: [
PlexLineSeries(name: 'Active', data: [(DateTime.now(), 400.0)]),
],
showArea: false,
)
| PlexLineSeries | Description |
|---|---|
name |
Series label |
data |
List<(DateTime, double)> โ (x, y) pairs |
PlexPieChart & PlexDonutChart
Pie and donut charts for proportional data.
PlexPieChart(
title: 'Share',
data: [PlexPieSegment('A', 40), PlexPieSegment('B', 60)],
)
PlexDonutChart(title: 'Share', data: [...])
| PlexPieSegment | Description |
|---|---|
label |
Segment label |
value |
Numeric value (proportional) |
PlexSparkline
Compact inline chart for KPI cards. Uses CustomPainter (no Syncfusion dependency).
PlexSparkline(
data: [10.0, 14.0, 12.0, 18.0, 22.0],
width: 80,
height: 32,
)
PlexChartGant
Hour-level Gantt chart for task scheduling.
PlexChartGant(
tasks: [
GantTask(name: 'Task 1', start: DateTime.now(), end: DateTime.now().add(Duration(hours: 2))),
],
)
Dashboard & KPI (v1.7.x)
PlexDashboardCard
KPI card with optional trend indicator and sparkline. Uses PlexCard internally.
PlexDashboardCard(
title: 'Revenue',
value: '\$12,450',
subtitle: 'vs last month',
trend: 12.4,
icon: Icon(Icons.trending_up),
chart: PlexSparkline(data: [10, 14, 12, 18, 22]),
onTap: () {},
)
| Parameter | Type | Description |
|---|---|---|
title |
String |
Card title |
value |
String |
Main value (e.g. KPI) |
subtitle |
String? |
Optional subtitle |
trend |
double? |
Positive = green up-arrow, negative = red down-arrow |
icon |
Widget? |
Leading icon |
chart |
Widget? |
Typically PlexSparkline |
onTap |
VoidCallback? |
Tap handler |
color |
Color? |
Optional accent color |
PlexDashboardGrid
Responsive grid of dashboard cards. Uses LayoutBuilder for responsive column count (2 small, 3 medium, 4 large screens).
PlexDashboardGrid(
cards: [
PlexDashboardCard(title: 'Users', value: '1,234'),
PlexDashboardCard(title: 'Orders', value: '56', trend: -3.2),
],
crossAxisSpacing: 16,
mainAxisSpacing: 16,
)
| Parameter | Description |
|---|---|
cards |
List<PlexDashboardCard> |
crossAxisCount |
Optional override (default: responsive) |
crossAxisSpacing |
Horizontal gap (default: 16) |
mainAxisSpacing |
Vertical gap (default: 16) |
Timeline & Calendar (v1.7.x)
PlexTimeline
Vertical timeline with optional alternating layout.
PlexTimeline(
events: [
PlexTimelineEvent(title: 'Order placed', subtitle: 'Details', timestamp: '10:00'),
PlexTimelineEvent(title: 'Shipped', icon: Icon(Icons.local_shipping)),
],
alternating: false,
dotRadius: 10.0,
)
| PlexTimelineEvent | Description |
|---|---|
title |
Event title |
subtitle |
Optional subtitle |
timestamp |
Optional timestamp string |
icon |
Optional leading icon |
color |
Optional dot/line color |
child |
Optional expanded content |
PlexCalendar
Calendar built on Syncfusion SfCalendar.
PlexCalendar(
events: [
PlexCalendarEvent(title: 'Meeting', start: DateTime.now(), end: DateTime.now().add(Duration(hours: 1))),
],
initialView: PlexCalendarView.month,
onEventTap: (event) {},
onSlotTap: (dateTime) {},
allowDragging: false,
)
| PlexCalendarEvent | Description |
|---|---|
title |
Event title |
start, end |
DateTime range |
color |
Optional Color |
data |
Optional custom data |
| PlexCalendarView | Description |
|---|---|
month |
Month view |
week |
Week view |
day |
Day view |
Rich Text Editor (v1.7.x)
PlexRichTextEditor
WYSIWYG editor using flutter_quill. Supports Delta JSON and HTML input.
PlexRichTextEditor(
initialValue: '[{"insert":"Hello\\n"}]',
outputFormat: PlexRichTextFormat.delta,
onChanged: (value) => print(value),
placeholder: 'Write something...',
minHeight: 200.0,
)
PlexFormFieldRichText
Form field wrapper for the rich text editor.
PlexFormFieldRichText(
properties: PlexFormFieldGeneric.title('Description'),
initialValue: null,
outputFormat: PlexRichTextFormat.html,
onChanged: (value) {},
)
Output formats: PlexRichTextFormat.delta (JSON), PlexRichTextFormat.html, PlexRichTextFormat.markdown
Backgrounds (v1.7.x)
PlexBackground
Decorative background layer. Supports five types:
PlexBackground(
type: PlexBackgroundType.particleField,
child: content,
)
| PlexBackgroundType | Description |
|---|---|
neoGlass |
Glassmorphic blur effect |
particleField |
Animated drifting particles (canvas-drawn circles) |
gradientMesh |
Animated multi-stop gradient rotation |
geometricTiles |
Tessellation pattern (hexagonal/triangular) |
solidSurface |
Plain color (for accessibility/performance; no animation) |
Forms & Inputs
PlexFormWidget & PlexFormField
Rapidly build forms from your model classes.
class User with PlexForm {
String name = '';
int age = 0;
@override
List<PlexFormField> getFields(State context) => [
PlexFormField.input(title: "Name", type: String, onChange: (v) => name = v),
PlexFormField.input(title: "Age", type: int, onChange: (v) => age = v),
];
}
// Usage:
PlexFormWidget<User>(entity: User(), onSubmit: (user) => print(user.name))
Specialized Form Fields
- Input:
PlexFormFieldInput - Date/Time:
PlexFormFieldDateโPlexFormFieldDateType.typeDate,typeTime,typeDateTime - Dropdown:
PlexFormFieldDropdown - Multi-Select:
PlexFormFieldMultiSelect - Autocomplete:
PlexFormFieldAutoComplete - Button:
PlexFormFieldButton - Stepper (v1.7.x):
PlexFormFieldStepperโ numeric +/- with min, max, step - Color (v1.7.x):
PlexFormFieldColorโ color picker with preset grid - File (v1.7.x):
PlexFormFieldFileโ file picker withallowedExtensions,allowMultiple - Rich Text (v1.7.x):
PlexFormFieldRichTextโ WYSIWYG editor
Example:
PlexFormFieldInput(
properties: PlexFormFieldGeneric(title: "Username"),
inputController: TextEditingController(),
)
PlexFormFieldDate(type: PlexFormFieldDateType.typeDate)
PlexFormFieldDropdown(dropdownItems: ["A", "B", "C"])
PlexFormFieldMultiSelect(dropdownItems: ["A", "B", "C"])
PlexFormFieldAutoComplete(autoCompleteItems: (query) async => ["A", "B", "C"])
PlexFormFieldButton(properties: PlexFormFieldGeneric(title: "Submit"), buttonClick: () {})
// v1.7.x additions
PlexFormFieldStepper(value: 5, min: 0, max: 10, step: 1, onChanged: (v) {})
PlexFormFieldColor(value: Colors.blue, onChanged: (c) {})
PlexFormFieldFile(allowedExtensions: ['pdf', 'doc'], allowMultiple: false, onChanged: (files) {})
PlexFormFieldRichText(properties: PlexFormFieldGeneric.title('Notes'), onChanged: (v) {})
| Form Field | Key Parameters |
|---|---|
PlexFormFieldStepper |
value, min, max, step, onChanged |
PlexFormFieldColor |
value, onChanged โ opens color grid dialog |
PlexFormFieldFile |
allowedExtensions, allowMultiple, onChanged โ uses file_picker |
PlexFormFieldRichText |
properties, initialValue, outputFormat, onChanged |
Conditional Fields (v1.7.x) โ showWhen
Show or hide fields based on form state.
PlexFormField.input(
title: "Company",
type: String,
onChange: (v) => entity.company = v,
showWhen: (state) => state.isBusiness == true,
)
PlexValidator (v1.7.x)
Reusable validators for form fields.
import 'package:plex/plex_utils/plex_validator.dart';
// Single validator
PlexFormFieldInput(
errorController: errorController,
inputOnChange: (v) {
final err = PlexValidator.required(message: 'Required')(v);
errorController?.setValue(err);
},
)
// Composed validators
final validate = PlexValidator.compose([
PlexValidator.required(),
PlexValidator.email(),
PlexValidator.minLength(8),
]);
// Available: required(), email(), minLength(n), maxLength(n), pattern(regex), compose([...])
PlexWizardForm (v1.7.x)
Multi-step form with Back / Next / Submit. Renders a Stepper with validation per step.
PlexWizardForm(
steps: [
PlexWizardStep(title: 'Step 1', fields: [/* PlexFormField list */]),
PlexWizardStep(title: 'Step 2', fields: [...]),
],
onComplete: () {},
onCancel: () {},
)
| PlexWizardStep | Description |
|---|---|
title |
Step title |
fields |
List<PlexFormField> for this step |
Form State Persistence (v1.7.x)
Auto-save and restore form drafts via PlexDb.
PlexFormWidget<User>(
entity: userForm,
onSubmit: (user) {},
persistenceKey: 'user_registration',
db: plexDb,
)
PlexInputWidget (Legacy)
A flexible input widget supporting text, dropdown, date, and multi-select. (Prefer the new form fields above.)
Layout & Navigation
PlexNavigationRail
A customizable navigation rail for side navigation in desktop/tablet layouts.
PlexNavigationRail(
destinations: [
PlexRoute(route: "/home", title: "Home", screen: (context) => HomeScreen()),
PlexRoute(route: "/settings", title: "Settings", screen: (context) => SettingsScreen()),
],
selectedDestination: 0,
onSelectDestination: (index) {},
)
PlexCard & PlexCardGlass
Material and glassmorphic card widgets for modern UIs.
PlexCard(child: Text("Standard Card"))
PlexCardGlass(child: Text("Glass Card"))
PlexBackground
Decorative background layer. Supports neoGlass, particleField, gradientMesh, geometricTiles, and solidSurface (v1.7.x).
PlexBackground(type: PlexBackgroundType.neoGlass, child: content)
PlexMenu
Model for menu items with icon and title.
PlexMenu("Dashboard", icon: Icon(Icons.dashboard))
Navigation & Routing (v1.5.x)
PLEX provides a pluggable navigation layer with support for route guards, parameterized paths, and optional GoRouter for deep linking and web URL sync.
Navigation API
Use the Plex class for all navigation. It delegates to the configured router (GetX by default).
import 'package:plex/plex_utils/plex_routing.dart';
// Push a named route
Plex.toNamed('/orders');
// Push with query parameters
Plex.toNamed('/orders', parameters: {'page': '1', 'status': 'pending'});
// Replace current route (e.g. login โ home)
Plex.offAndToNamed('/home');
// Push a widget directly (GetX router only)
Plex.to(OrderDetailScreen(orderId: 42), arguments: order);
// Pop the current route
Plex.back();
Plex.back(result: selectedItem);
PlexRouter โ Pluggable Backend
PlexApp accepts an optional router or experimentalRouter parameter. The default is PlexGetXRouter (wraps GetX).
// Default: GetX (unchanged behavior)
runApp(PlexApp(appInfo: ..., dashboardConfig: ...));
// Custom router
runApp(PlexApp(appInfo: ..., router: myCustomRouter, dashboardConfig: ...));
// Experimental: GoRouter (deep linking, web URL sync)
import 'package:plex/plex_router/plex_go_router.dart';
runApp(PlexApp(appInfo: ..., experimentalRouter: PlexGoRouter(), dashboardConfig: ...));
Route Guards
Guards run before navigation and can redirect. Use PlexAuthGuard and PlexRoleGuard, or implement PlexRouteGuard.
import 'package:plex/plex_router/plex_route_guard.dart';
// Require authentication
PlexRoute(
route: '/profile',
title: 'Profile',
screen: (context) => ProfileScreen(),
guards: [PlexAuthGuard(loginPath: '/Login')],
);
// Require a specific role (or use legacy rule field โ auto-wrapped)
PlexRoute(
route: '/admin',
title: 'Admin',
screen: (context) => AdminScreen(),
guards: [PlexRoleGuard('admin')],
);
// Legacy: rule is auto-wrapped into PlexRoleGuard
PlexRoute(route: '/reports', title: 'Reports', screen: ..., rule: 'view_reports');
Parameterized Paths (GoRouter)
When using PlexGoRouter, set path for parameterized routes. The URL bar reflects the current route on web.
PlexRoute(
route: '/orders',
path: '/orders/:id',
title: 'Order Detail',
screen: (context, {data}) => OrderDetailScreen(orderId: data),
);
// Navigate with parameters
Plex.toNamed('/orders', parameters: {'id': '42'});
PlexRoute Reference
| Field | Type | Description |
|---|---|---|
route |
String |
Route name used for navigation |
path |
String? |
Parameterized path for GoRouter (e.g. "/orders/:id") |
title |
String |
Display title |
screen |
Widget Function(BuildContext, {dynamic data}) |
Screen builder |
rule |
String? |
Permission rule (auto-wraps to PlexRoleGuard) |
guards |
List<PlexRouteGuard> |
Route guards (evaluated before navigation) |
external |
bool |
If true, pushed onto stack instead of inline in dashboard |
category |
String |
Groups routes in drawer menu |
Loading, Feedback & Effects
PlexLoaderV1 / PlexLoaderV2
Show loading indicators (two styles).
PlexLoaderV1()
PlexLoaderV2()
PlexShimmer
Show shimmer effect while loading data.
PlexShimmer(child: Container(width: 200, height: 20))
PlexInfoSheet
Highly configurable bottom sheet for info, errors, alerts, etc.
PlexInfoSheet.show(
context,
title: "Info",
message: "This is an info sheet.",
type: PlexInfoSheetType.info,
)
| PlexInfoSheetType | Description |
|---|---|
info |
Informational |
error |
Error state |
alert |
Warning/alert |
PlexSelectionList
Show a searchable, selectable list in a modal.
showPlexSelectionList(
context,
items: ["A", "B", "C"],
itemText: (item) => item,
onSelect: (item) => print(item),
)
PlexHighlightWidget
Highlight a widget with animation.
PlexHighlightWidget(child: Text("Highlight Me!"))
Localization & Accessibility (v1.9.x)
PLEX provides built-in i18n support via an overridable string catalog and systematic accessibility annotations.
PlexStrings โ String Catalog
Subclass PlexStrings and override getters to provide custom translations. All user-visible strings in PLEX widgets use context.plexStrings.X.
import 'package:plex/plex_l10n/plex_strings.dart';
import 'package:plex/plex_l10n/plex_localization.dart';
// Subclass for Arabic
class ArabicPlexStrings extends PlexStrings {
@override
String get loginTitle => 'ุชุณุฌูู ุงูุฏุฎูู';
@override
String get loginUsername => 'ุงุณู
ุงูู
ุณุชุฎุฏู
';
@override
String get loginButton => 'ุฏุฎูู';
// Override only what you need; others fall back to English
}
// Subclass for French
class FrenchPlexStrings extends PlexStrings {
@override
String get loginTitle => 'Connexion';
@override
String get formSubmit => 'Soumettre';
}
PlexLocalizationConfig โ Setup in PlexApp
runApp(PlexApp(
appInfo: ...,
dashboardConfig: ...,
localizationConfig: PlexLocalizationConfig(
supportedLocales: [Locale('en'), Locale('ar'), Locale('fr')],
translationLoader: (locale) {
switch (locale.languageCode) {
case 'ar': return ArabicPlexStrings();
case 'fr': return FrenchPlexStrings();
default: return PlexStrings();
}
},
),
));
context.plexStrings โ Usage in Widgets
import 'package:plex/plex_l10n/plex_localization.dart';
// In any widget with BuildContext
Text(context.plexStrings.loginTitle)
PlexFormFieldGeneric.title(context.plexStrings.loginUsername)
PlexAccessibilityConfig โ Options
| Option | Description |
|---|---|
highContrast |
Forces high-contrast ColorScheme |
largeText |
Applies 1.3ร text scale factor |
reduceMotion |
Disables PlexBackground animations |
runApp(PlexApp(
appInfo: ...,
accessibilityConfig: PlexAccessibilityConfig(
highContrast: true,
largeText: true,
reduceMotion: false,
),
));
semanticLabel โ Form Fields & Highlight Widget
| Widget | Parameter | Fallback |
|---|---|---|
PlexFormFieldGeneric |
semanticLabel |
title when null |
PlexHighlightWidget |
semanticLabel |
'Notification' when null |
PlexFormFieldInput(
properties: PlexFormFieldGeneric(
title: 'Email',
semanticLabel: 'Email address input',
),
)
PlexHighlightWidget(
enabled: true,
semanticLabel: 'Unread messages',
child: Icon(Icons.mail),
)
Phase 9 โ Complete Setup Example
End-to-end setup for localization and accessibility:
import 'package:flutter/material.dart';
import 'package:plex/plex_package.dart';
import 'package:plex/plex_l10n/plex_strings.dart';
import 'package:plex/plex_l10n/plex_localization.dart';
import 'package:plex/plex_accessibility/plex_accessibility.dart';
// 1. Subclass PlexStrings for your language
class FrenchPlexStrings extends PlexStrings {
@override
String get loginTitle => 'Connexion';
@override
String get loginUsername => 'Nom d\'utilisateur';
@override
String get loginPassword => 'Mot de passe';
@override
String get loginButton => 'Se connecter';
@override
String get formSubmit => 'Soumettre';
@override
String get wizardBack => 'Retour';
@override
String get wizardNext => 'Suivant';
@override
String get wizardSubmit => 'Soumettre';
@override
String get dialogOk => 'OK';
@override
String get dialogCancel => 'Annuler';
}
void main() {
runApp(PlexApp(
appInfo: PlexAppInfo(
title: 'My App',
appLogo: Icon(Icons.apps),
initialRoute: '/home',
),
dashboardConfig: PlexDashboardConfig(
dashboardScreens: [
PlexRoute(route: '/home', title: 'Home', screen: (ctx) => HomeScreen()),
],
),
// 2. Wire localization
localizationConfig: PlexLocalizationConfig(
supportedLocales: [Locale('en'), Locale('fr')],
translationLoader: (locale) {
if (locale.languageCode == 'fr') return FrenchPlexStrings();
return PlexStrings();
},
),
// 3. Wire accessibility
accessibilityConfig: PlexAccessibilityConfig(
highContrast: false,
largeText: false,
reduceMotion: false,
),
));
}
// 4. Use context.plexStrings in your widgets
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(context.plexStrings.loginTitle),
),
);
}
}
State Management & Reactivity
PlexWidget & PlexWidgetController
A reactive widget that can be updated via a controller, replacing BLoC/provider for simple cases.
final controller = PlexWidgetController();
PlexWidget(controller: controller, createWidget: (context, data) => Text("Current value: $data"))
PlexRx & PlexRxWidget
Simple observable/reactive state management.
final count = 0.plexObs;
Text("Count: ").plexRxWidget(count)
PlexAsyncAction & runAction
Eliminates boilerplate for async operations with automatic loading and error handling. Use with PlexViewModel, PlexViewViewModel, or directly on PlexState / PlexViewState.
import 'package:plex/plex_view_model/plex_async_action.dart';
// In a PlexViewModel
void fetchOrders() => runAction(PlexAsyncAction(
() => orderService.getOrders(),
onSuccess: (orders) => setState(() => _orders = orders),
onError: (e, s) => context?.showMessage('Failed to load orders'),
));
// In PlexState or PlexViewState directly (no ViewModel needed)
void onRefresh() => runAction(PlexAsyncAction(
() => PlexApi.instance.get('/data'),
onSuccess: (result) => setState(() => _data = result.data),
onError: (e, s) => PlexLogger.e('Refresh', 'Error', error: e),
));
runAction automatically calls showLoading() before the task, hideLoading() in finally, catches errors, invokes onError, and logs via PlexLogger. Returns null on error, or the result on success.
Utilities & Helpers
Spacing & Dimensions
spaceMini() // Widget with 2px
spaceSmall() // Widget with 8px
spaceMedium() // Widget with 16px
PlexDim.large // 32.0
PlexLogger (Structured Logging)
Structured logging with configurable levels. In release builds, verbose and debug are suppressed by default.
import 'package:plex/plex_utils/plex_logger.dart';
// Log at different levels
PlexLogger.v('MyTag', 'Verbose'); // verbose
PlexLogger.d('MyTag', 'Debug message'); // debug
PlexLogger.i('MyTag', 'Info message'); // info
PlexLogger.w('MyTag', 'Warning', error: someError);
PlexLogger.e('MyTag', 'Error occurred', error: e, stack: stackTrace);
// Configure minimum level (messages below this are not output)
PlexLogger.setLevel(PlexLogLevel.info);
| PlexLogLevel | Description |
|---|---|
verbose |
Most verbose |
debug |
Debug messages |
info |
Informational |
warning |
Warnings |
error |
Errors |
In release builds, verbose and debug are suppressed by default.
Add remote sink (e.g. Sentry, Crashlytics):
class MyLogSink extends PlexLogSink {
@override
void write(PlexLogLevel level, String tag, String message, {Object? error, StackTrace? stack}) {
// Send to your logging service
}
}
PlexLogger.addSink(MyLogSink());
PlexLogger.removeSink(myLogSink);
Console & Async
console("Debug message") // Uses PlexLogger internally; prefer PlexLogger for new code
delay(() => print("Delayed"), delayMillis: 500)
runAsync(() => print("Async"))
Platform & Screen Size
isLargeScreen(context)
isMediumScreen(context)
isSmallScreen(context)
Date & String Utilities
DateTime.now().toDDMMMHHmmss()
"2012-02-27 13:27:00".toDate()
Grouping & Sorting
List<T>.sortAndReturn()
List<T>.groupBy((item) => key)
Other Widgets
PlexScanner
Barcode/QR code scanner widget.
PlexScanner()
Networking & API
PLEX provides a full-featured HTTP client with interceptors, timeouts, cancellation, caching, and type-safe response parsing.
Basic Setup
import 'package:plex/plex_networking/plex_api_calls.dart';
// Configure base URL and headers (typically in main() or app init)
PlexApi.instance.setBaseUrl('https://api.example.com');
PlexApi.instance.setHeadersCallback(() async => {
'Content-Type': 'application/json',
'X-App-Version': '1.0.0',
});
Simple API Calls
// GET
final result = await PlexApi.instance.get('/users', queryParams: {'page': 1});
if (result.success) {
final data = result.data; // Parsed JSON
}
// POST
final result = await PlexApi.instance.post('/users', body: {'name': 'John', 'email': 'john@example.com'});
// PUT
final result = await PlexApi.instance.put('/users/1', body: {'name': 'John Updated'});
// DELETE
final result = await PlexApi.instance.delete('/users/1');
// File upload (multipart)
final result = await PlexApi.instance.uploadFiles(
'/upload',
formData: {'description': 'Profile photo'},
files: {'file': File('/path/to/image.jpg')},
);
// File download
await PlexNetworking.instance.downloadFile(
'https://example.com/file.pdf',
filename: 'document.pdf',
onProgressUpdate: (downloaded, percentage, file) {
print('Progress: $percentage%');
},
);
Type-Safe Response Parsing
Use getTyped and postTyped to parse JSON responses into strongly-typed models:
class User {
final String id;
final String name;
User({required this.id, required this.name});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
name: json['name'] as String,
);
}
// GET with typed response
final result = await PlexApi.instance.getTyped<User>(
'/users/1',
fromJson: User.fromJson,
);
if (result.success) {
final user = result.data as User; // Already typed
}
// POST with typed response
final result = await PlexApi.instance.postTyped<User>(
'/users',
body: {'name': 'Jane'},
fromJson: User.fromJson,
);
Interceptors
Add request/response hooks for auth, retries, logging, etc.:
import 'package:plex/plex_networking/plex_networking.dart';
import 'package:plex/plex_networking/plex_interceptor.dart';
// Add Bearer token (automatic on every request)
PlexNetworking.instance.addInterceptor(
PlexAuthInterceptor(() async => await getStoredToken()),
);
// Retry on 500/502/503 (default: 3 attempts)
PlexNetworking.instance.addInterceptor(
PlexRetryInterceptor(maxAttempts: 3, retryOnStatusCodes: [500, 502, 503]),
);
// Custom interceptor
class LoggingInterceptor extends PlexInterceptor {
@override
Future<Map<String, String>> onRequest(String url, Map<String, String> headers) async {
PlexLogger.d('API', 'Request: $url');
return headers;
}
@override
Future<PlexApiResponse> onResponse(PlexApiResponse response) async => response;
}
PlexNetworking.instance.addInterceptor(LoggingInterceptor());
Request Cancellation & Timeouts
// Create a cancel token
final cancelToken = PlexCancelToken();
// Use with low-level PlexNetworking (e.g. for search/cancel)
final response = await PlexNetworking.instance.get(
'/search',
query: {'q': 'query'},
timeout: Duration(seconds: 10),
cancelToken: cancelToken,
);
// Cancel from another part of your app (e.g. user taps "Cancel")
cancelToken.cancel();
// Default timeout is 30 seconds; override per request:
PlexNetworking.instance.defaultTimeout = Duration(seconds: 60);
Response Caching
Cache GET responses to reduce network calls and improve offline support:
import 'package:plex/plex_networking/plex_networking.dart';
import 'package:plex/plex_networking/plex_cache.dart';
import 'package:plex/plex_database/plex_database.dart';
// Initialize PlexDb first (e.g. in main)
final db = await PlexDb.initialize(PlexDbConfig('my_app'));
// Enable cache
await PlexNetworking.instance.enableCache(
PlexCacheConfig(
maxAge: Duration(minutes: 5),
maxStale: Duration(minutes: 30), // Optional: serve stale when offline
cacheKey: (url, query) => '$url?${query?.toString() ?? ''}', // Optional custom key
),
db,
);
// GET requests now check cache first; successful responses are cached automatically
final result = await PlexApi.instance.get('/products');
// Clear cache when needed
await PlexNetworking.instance.clearCache();
await PlexNetworking.instance.clearCache(urlPattern: '/products'); // Clear only matching
Error Handling
The API returns a structured error hierarchy. Check PlexApiResult:
final result = await PlexApi.instance.get('/users', queryParams: {'page': 1});
if (result.success) {
final data = result.data;
} else {
// result.code: 408 = timeout, 5001 = no network, 499 = cancelled, 4xx/5xx = server error
// result.message: human-readable error
switch (result.code) {
case 408: showError('Request timed out'); break;
case 5001: showError('No internet connection'); break;
case 499: showError('Request cancelled'); break;
default: showError(result.message);
}
}
When using PlexNetworking directly, you get PlexApiResponse which can be pattern-matched:
final response = await PlexNetworking.instance.get('/users');
if (response is PlexSuccess) {
final data = response.response;
} else if (response is PlexNetworkTimeout) {
// Handle timeout
} else if (response is PlexNetworkNoConnectivity) {
// Handle no network
} else if (response is PlexNetworkServerError) {
final statusCode = response.statusCode;
final body = response.body;
}
Data Storage
PlexSp (SharedPreferences)
Lightweight key-value storage for user preferences and small data:
import 'package:plex/plex_sp.dart';
await PlexSp.instance.initialize();
// Set values
PlexSp.instance.setString('theme', 'dark');
PlexSp.instance.setBool('notifications', true);
PlexSp.instance.setInt('counter', 42);
PlexSp.instance.setList('tags', ['a', 'b', 'c']);
// Get values
final theme = PlexSp.instance.getString('theme');
final enabled = PlexSp.instance.getBool('notifications');
// Remove (pass null)
PlexSp.instance.setString('theme', null);
// Check key exists
if (PlexSp.instance.hasKey('theme')) { }
Built-in constants: PlexSp.loggedInUser, PlexSp.rememberUsers.
PlexDb (Sembast)
Local NoSQL database for structured data. Import: package:plex/plex_database/plex_database.dart
// Initialize
final db = await PlexDb.initialize(PlexDbConfig('my_app'));
// Generic collection (key-value)
final collection = db.getCollection('items');
await collection.insert({'name': 'Item 1', 'value': 100});
final all = await collection.getAll();
final one = await collection.getById(1);
// Typed entity collection (model must use PlexEntity mixin)
// PlexEntity provides: int? entityId (set by DB on insert)
class Task with PlexEntity {
String title;
bool done;
Task({required this.title, this.done = false});
factory Task.fromJson(Map<String, dynamic> m) => Task(title: m['title'], done: m['done'] ?? false);
Map<String, dynamic> toJson() => {'title': title, 'done': done};
}
final tasks = db.getEntityCollection<Task>('tasks', fromJson: Task.fromJson, toJson: (t) => t.toJson());
await tasks.insert(Task(title: 'Buy milk'));
| PlexDbConfig | Type | Description |
|---|---|---|
dbName |
String |
Database file name |
encrypted |
bool |
Enable AES-256 encryption at rest (default: false) |
| PlexCollection methods | Description |
|---|---|
insert(record) |
Insert a new record (auto-generates entityId) |
insertAll(records) |
Bulk insert |
update(record) |
Update by entityId |
delete(record) |
Delete by record |
deleteById(id) |
Delete by id |
getById(id) |
Get single record |
getAll() |
Get all records |
find({limit, offset}) |
Find with pagination |
Database Enhancements (v1.8.x)
Query Builder โ Fluent DSL for filtering, sorting, and pagination:
import 'package:plex/plex_database/plex_database.dart';
final orders = db.getEntityCollection<Order>('orders', fromJson: Order.fromJson, toJson: (o) => o.toJson());
// Query with filters
final pending = await orders
.query()
.where('status').equals('pending')
.where('total').greaterThan(100)
.orderBy('createdAt', descending: true)
.limit(20)
.get();
// Count, first, delete
final count = await orders.query().where('status').equals('pending').count();
final first = await orders.query().where('status').equals('pending').first();
await orders.query().where('archived').equals(true).deleteAll();
PlexQuery conditions โ All return PlexQuery<T> for chaining:
| Method | Example |
|---|---|
equals(value) |
where('status').equals('pending') |
notEquals(value) |
where('status').notEquals('archived') |
greaterThan(value) |
where('total').greaterThan(100) |
greaterThanOrEquals(value) |
where('count').greaterThanOrEquals(10) |
lessThan(value) |
where('total').lessThan(1000) |
lessThanOrEquals(value) |
where('count').lessThanOrEquals(10) |
contains(value, {caseInsensitive}) |
where('name').contains('john', caseInsensitive: true) |
isIn(List values) |
where('status').isIn(['pending', 'active']) |
isNull() |
where('deletedAt').isNull() |
isNotNull() |
where('deletedAt').isNotNull() |
PlexQuery terminal methods:
| Method | Returns | Description |
|---|---|---|
get() |
Future<List<T>> |
Execute and return all matching entities |
first() |
Future<T?> |
Return first match or null |
count() |
Future<int> |
Count matching records |
deleteAll() |
Future<void> |
Delete all matching records |
watch() |
Stream<List<T>> |
Reactive stream; emits when data changes |
Migrations โ Versioned schema changes. Migrations run in ascending order on startup. Import: package:plex/plex_database/plex_migration.dart (or re-exported from plex_database.dart).
final db = await PlexDb.initialize(
PlexDbConfig('my_app'),
migrations: [
PlexDbMigration(version: 1, up: (db) async {
// Add initial collections
}),
PlexDbMigration(version: 2, up: (db) async {
// Add indexes, new collections
}),
],
);
| PlexDbMigration | Description |
|---|---|
version |
Integer; migrations run in ascending order |
up(db) |
Async callback; receives PlexDb (e.g. db.getCollection('orders')) |
Relations โ hasMany and belongsTo. Import: package:plex/plex_database/plex_relation.dart
final orderItems = orderCollection.hasMany(itemCollection, 'orderId');
final items = await orderItems.loadHasMany(order.entityId!);
final customer = orderCollection.belongsTo(customerCollection, 'customerId');
final cust = await customer.loadBelongsTo(order);
| Relation | Usage | Load method |
|---|---|---|
hasMany(related, foreignKey) |
One-to-many (e.g. Order โ OrderItems) | loadHasMany(ownerId) โ Future<List<R>> |
belongsTo(related, localForeignKey) |
Many-to-one (e.g. Order โ Customer) | loadBelongsTo(owner) โ Future<R?> |
Reactive Queries โ Streams that emit when data changes:
// Watch all entities
orders.watchAll().listen((list) => updateUI(list));
// Watch a single entity
orders.watchById(42).listen((order) => updateDetail(order));
// Watch a filtered query
orders.query().where('status').equals('pending').watch().listen((list) => ...);
Encryption at Rest โ AES-256-CBC encryption with key stored in secure storage:
final db = await PlexDb.initialize(PlexDbConfig('my_app', encrypted: true));
- Uses
flutter_secure_storageandencrypt(included in Plex) - Key is generated and stored in the device keystore on first run
- Key stored under
plex_db_key; cannot be changed without losing access to encrypted data
PlexDb HTTP cache โ Used by PlexNetworking for response caching. Also available for custom use:
await db.putInCache('my_key', {'data': 'value'});
final cached = await db.getFromCache('my_key');
await db.deleteFromCache('my_key');
await db.clearCache(); // Clear all
await db.clearCache(urlPattern: '/api'); // Clear matching keys
final keys = await db.getCacheKeys();
Dependency Injection
PLEX provides a flexible, tag-based dependency injection system with support for global and scoped lifetimes, circular dependency detection, and async initialization.
Basic Registration & Resolution
import 'package:plex/plex_di/plex_dependency_injection.dart';
// Register singleton (created immediately)
injectSingleton(MyService());
// Register lazy singleton (created on first access)
injectSingletonLazy((parm) => MyService());
// Register factory (new instance each time)
injectFactory((parm) => MyService());
// Resolve
final service = fromPlex<MyService>();
// Use tags for multiple implementations of same type
injectSingleton(HttpClient(), tag: 'api');
injectSingleton(HttpClient(), tag: 'auth');
final apiClient = fromPlex<HttpClient>(tag: 'api');
Scoped DI (v1.4.x)
Scoped dependencies live in a named scope and are cleaned up when the scope is closed. Use this for session-level or screen-level services that should not persist forever.
// Register a dependency in a named scope (lazy within scope)
injectScoped<SessionService>(
() => SessionService(),
scope: 'session',
tag: null, // optional tag
);
// Resolve from scope (falls back to global if not found in scope)
final session = fromScoped<SessionService>(scope: 'session');
// Close a scope and dispose all PlexDisposable instances inside it
await closeScope('session');
When to use scoped DI:
- Session-scoped services (e.g., per-user session, checkout flow)
- Screen-scoped services that should be disposed when the screen is popped
- Feature modules that need isolated dependency graphs
PlexDisposable โ Cleanup on Scope Close
Implement the PlexDisposable mixin so your service receives a dispose() call when its scope is closed:
class MyScopedService with PlexDisposable {
StreamSubscription? _subscription;
@override
Future<void> dispose() async {
await _subscription?.cancel();
// Release resources, close connections, etc.
}
}
injectScoped<MyScopedService>(
() => MyScopedService(),
scope: 'feature',
);
// When closeScope('feature') is called, dispose() is invoked automatically
await closeScope('feature');
Services that do not implement PlexDisposable are simply removed from the scope; no dispose call is made.
Screen-Level Auto-Scope (PlexState)
PlexState supports an optional diScope override. When set, the scope is automatically closed when the screen is disposed (e.g., on pop or route replacement).
class OrderScreen extends PlexScreen {
@override
State<OrderScreen> createState() => _OrderScreenState();
}
class _OrderScreenState extends PlexState<OrderScreen> {
@override
String? get diScope => 'order_screen'; // Scope name for this screen
@override
void initState() {
super.initState();
// Register screen-scoped dependencies
injectScoped<OrderViewModel>(
() => OrderViewModel(),
scope: diScope!,
);
}
@override
Widget buildBody() {
final vm = fromScoped<OrderViewModel>(scope: diScope!);
return OrderView(viewModel: vm);
}
// When the screen is disposed, closeScope('order_screen') is called automatically
}
Screens that do not override diScope (default null) are unaffected.
Circular Dependency Detection
The DI container detects circular dependencies during resolution and throws PlexCircularDependencyError with the full dependency chain instead of causing a stack overflow.
// Example: A depends on B, B depends on A
injectSingletonLazy<ServiceA>((_) => ServiceA()); // ServiceA() calls fromPlex<ServiceB>()
injectSingletonLazy<ServiceB>((_) => ServiceB()); // ServiceB() calls fromPlex<ServiceA>()
try {
fromPlex<ServiceA>();
} on PlexCircularDependencyError catch (e) {
print(e); // "PlexCircularDependencyError: Circular dependency detected: ServiceA โ ServiceB โ ServiceA"
}
Async Lazy Initialization
For dependencies that require async setup (e.g., database initialization, network config), use injectSingletonLazyAsync and fromPlexAsync:
// In main() or app setup
injectSingletonLazyAsync<PlexDb>(
() => PlexDb.initialize(PlexDbConfig('my_app')),
);
// In a ViewModel or service โ await on first access
final db = await fromPlexAsync<PlexDb>();
// With tag
injectSingletonLazyAsync<AnalyticsService>(
() => AnalyticsService.init(),
tag: 'analytics',
);
final analytics = await fromPlexAsync<AnalyticsService>(tag: 'analytics');
The async builder is invoked once; subsequent calls to fromPlexAsync return the same cached instance.
API Reference
| Function | Description |
|---|---|
injectSingleton<T>(instance, {tag}) |
Register an immediate singleton |
injectSingletonLazy<T>(builder, {tag}) |
Register a lazy singleton (sync builder) |
injectSingletonLazyAsync<T>(builder, {tag}) |
Register a lazy singleton (async builder) |
injectFactory<T>(builder, {tag}) |
Register a factory (new instance each time) |
injectScoped<T>(builder, {scope, tag}) |
Register a scoped lazy singleton |
fromPlex<T>({tag, parm}) |
Resolve from global registry |
fromPlexAsync<T>({tag}) |
Resolve async lazy singleton |
fromScoped<T>({scope, tag, parm}) |
Resolve from scope (fallback to global) |
closeScope(scope) |
Close scope and dispose PlexDisposable instances |
| Type / Mixin | Description |
|---|---|
PlexDisposable |
Mixin with Future<void> dispose() for cleanup on scope close |
PlexCircularDependencyError |
Error thrown when a circular dependency is detected; chain contains the resolution path |
Real-Time & Networking
PlexSignalR
Real-time communication using SignalR.
PlexSignalR.config = PlexSignalRConfig(
"https://serverurl:port", "hubPath",
remoteMethods: [PlexSignalRMethod("OnEvent", (args) => print(args))],
);
await PlexSignalR.instance.start();
๐ธ Screenshots
| Material 3 Light | Material 3 Dark | Material 2 Light | Material 2 Dark |
|---|---|---|---|
| M3 Light | M3 Dark | M2 Light | M2 Dark |
More examples in the /screenshots folder.
๐ฆ Import Paths
| Module | Import |
|---|---|
| Core app | package:plex/plex_package.dart |
| Localization | package:plex/plex_l10n/plex_localization.dart, package:plex/plex_l10n/plex_strings.dart |
| Accessibility | package:plex/plex_accessibility/plex_accessibility.dart |
| Database | package:plex/plex_database/plex_database.dart |
| Migrations | package:plex/plex_database/plex_migration.dart |
| Relations | package:plex/plex_database/plex_relation.dart |
| Networking | package:plex/plex_networking/plex_networking.dart, plex_api_calls.dart, plex_cache.dart, plex_interceptor.dart |
| DI | package:plex/plex_di/plex_dependency_injection.dart |
| Routing | package:plex/plex_utils/plex_routing.dart |
| Guards | package:plex/plex_router/plex_route_guard.dart |
| GoRouter | package:plex/plex_router/plex_go_router.dart |
| Validator | package:plex/plex_utils/plex_validator.dart |
| Logger | package:plex/plex_utils/plex_logger.dart |
| Storage | package:plex/plex_sp.dart |
๐ Getting Started
Add PLEX to your pubspec.yaml:
dependencies:
plex: ^<latest_version>
Then run:
flutter pub get
Key dependencies (included in Plex): sembast, flutter_secure_storage, encrypt, syncfusion_flutter_*, flutter_quill, file_picker, get, go_router, connectivity_plus, path_provider, http, toastification, intl, lottie, mobile_scanner, signalr_netcore, and more. See pubspec.yaml for the full list.
๐งช Testing
PLEX includes built-in unit and widget tests. Run all tests:
flutter test
Or run specific test suites:
flutter test test/unit/
flutter test test/widget/
Test structure:
test/unit/โ Unit tests forPlexSp,PlexDI,PlexNetworking,PlexDb,PlexWidgetControllertest/widget/โ Widget tests forPlexFormFieldInput,PlexFormFieldDropdown
Key testable modules: PlexSp, PlexDI, PlexNetworking, PlexDb, PlexWidgetController, PlexFormFieldInput, PlexFormFieldDropdown
Dev dependencies (used when contributing to PLEX):
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^1.0.4
fake_async: ^1.3.2
path_provider_platform_interface: ^2.1.2
๐ ๏ธ Usage
Quick App Scaffold
import 'package:flutter/material.dart';
import 'package:plex/plex_package.dart';
void main() {
runApp(PlexApp(
appInfo: PlexAppInfo(
title: "My Enterprise App",
appLogo: Icon(Icons.business),
initialRoute: "/dashboard",
),
dashboardConfig: PlexDashboardConfig(
dashboardScreens: [
// Define your screens: PlexRoute(route, title, screen: (ctx) => YourScreen()),
],
),
));
}
// Navigate: Plex.toNamed('/path'); Plex.back();
Advanced Data Table
PlexDataTable(
columns: [PlexDataCell.text("ID"), PlexDataCell.text("Name")],
rows: [
[PlexDataCell.text("1"), PlexDataCell.text("Alice")],
[PlexDataCell.text("2"), PlexDataCell.text("Bob")],
],
)
// Unified API (v1.7.x) โ list, paginated, or stream
PlexDataTableUnified(
source: PlexTableSource.list(rows),
columns: [PlexDataCell.text("ID"), PlexDataCell.text("Name")],
features: PlexTableFeatures(search: true, print: true),
)
Form Builder from Model
class Order with PlexForm {
late String id;
late double amount;
Order();
@override
List<PlexFormField> getFields(State context) => [
PlexFormField.input(title: "ID", type: String, onChange: (v) => id = v),
PlexFormField.input(title: "Amount", type: double, onChange: (v) => amount = v),
];
}
MVVM ViewModel Example
import 'package:plex/plex_view_model/plex_async_action.dart';
class HomeScreenViewModel extends PlexViewModel<HomeScreen, _HomeScreenState> {
void fetchData() => runAction(PlexAsyncAction(
() => PlexApi.instance.get('/api/data'),
onSuccess: (result) {
if (result.success) setState(() => _data = result.data);
},
onError: (e, s) => context?.showMessage('Failed to load data'),
));
}
Dependency Injection
import 'package:plex/plex_di/plex_dependency_injection.dart';
injectSingleton(MyService());
final service = fromPlex<MyService>();
// Scoped (cleaned up when scope closes)
injectScoped<SessionService>(() => SessionService(), scope: 'session');
final session = fromScoped<SessionService>(scope: 'session');
// Async (e.g. database init)
injectSingletonLazyAsync(() => PlexDb.initialize(PlexDbConfig('my_app')));
final db = await fromPlexAsync<PlexDb>();
Navigation
import 'package:plex/plex_utils/plex_routing.dart';
Plex.toNamed('/orders');
Plex.offAndToNamed('/home'); // Replace stack (e.g. after login)
Plex.back(result: selectedItem);
// With GoRouter: parameterized paths and web URL sync
PlexApp(experimentalRouter: PlexGoRouter(), ...);
SignalR Real-Time Integration
PlexSignalR.config = PlexSignalRConfig(
"https://serverurl:port", "hubPath",
remoteMethods: [PlexSignalRMethod("OnEvent", (args) => print(args))],
);
await PlexSignalR.instance.start();
Complete API Setup Example
import 'package:flutter/material.dart';
import 'package:plex/plex_sp.dart';
import 'package:plex/plex_database/plex_database.dart';
import 'package:plex/plex_networking/plex_api_calls.dart';
import 'package:plex/plex_networking/plex_networking.dart';
import 'package:plex/plex_networking/plex_interceptor.dart';
import 'package:plex/plex_networking/plex_cache.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize storage
await PlexSp.instance.initialize();
final db = await PlexDb.initialize(PlexDbConfig('my_app'));
// Configure networking
PlexApi.instance.setBaseUrl('https://api.example.com');
PlexApi.instance.setHeadersCallback(() async => {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${await getToken()}',
});
// Optional: Add auth interceptor (alternative to setHeadersCallback)
PlexNetworking.instance.addInterceptor(
PlexAuthInterceptor(() async => await getToken()),
);
// Optional: Enable response caching
await PlexNetworking.instance.enableCache(
PlexCacheConfig(maxAge: Duration(minutes: 5)),
db,
);
runApp(MyApp());
}
๐๏ธ Architecture & Extensibility
- MVVM Pattern: Clean separation of UI and business logic.
- Customizable Themes: Use your own color schemes, images, or Material versions.
- Flexible Routing: Pluggable router (GetX or GoRouter), route guards, parameterized paths, and deep linking.
- Widget Extensibility: All core widgets are designed for extension and override.
๐ค Contributing
PLEX is open source and welcomes contributions! To get started:
- Fork the repository
- Create a new branch (
git checkout -b feature/your-feature) - Make your changes
- Submit a pull request
Please see the CONTRIBUTING.md (if available) for guidelines.
๐ License
This project is licensed under the MIT License. See the LICENSE file for details.
PLEX is built with โค๏ธ for the Flutter community. For questions, issues, or feature requests, please open an issue on GitHub.
Libraries
- plex_accessibility/plex_accessibility
- plex_annotations/plex_annotation_builders/plex_annotation_builders
- plex_annotations/plex_annotations
- plex_annotations/plex_generators/plex_model_generators
- plex_annotations/plex_visitors/plex_model_visitor
- plex_assets
- plex_charts/plex_bar_chart
- plex_charts/plex_chart_gant
- plex_charts/plex_line_chart
- plex_charts/plex_pie_chart
- plex_charts/plex_sparkline
- plex_database/plex_collection
- plex_database/plex_database
- plex_database/plex_db_codec
- plex_database/plex_entity
- plex_database/plex_entity_collection
- plex_database/plex_migration
- Re-exports PlexDbMigration and runMigrations from the main database module.
- plex_database/plex_query
- plex_database/plex_relation
- plex_di/plex_dependency_injection
- plex_extensions
- plex_l10n/plex_localization
- plex_l10n/plex_strings
- plex_networking/plex_api_calls
- plex_networking/plex_cache
- plex_networking/plex_interceptor
- plex_networking/plex_networking
- plex_package
- plex_route
- plex_router/plex_getx_router
- plex_router/plex_go_router
- plex_router/plex_route_guard
- plex_router/plex_router
- plex_rx/plex_rx
- plex_scanner
- plex_screens/plex_login_screen
- plex_screens/plex_screen
- plex_screens/plex_view
- plex_scrollview
- plex_signal_r/plex_signal_r
- plex_sp
- plex_theme
- plex_user
- plex_utils
- plex_utils/plex_date_utils
- plex_utils/plex_dimensions
- plex_utils/plex_logger
- plex_utils/plex_material
- plex_utils/plex_messages
- plex_utils/plex_pair
- plex_utils/plex_printer
- plex_utils/plex_routing
- plex_utils/plex_utils
- plex_utils/plex_validator
- plex_utils/plex_widgets
- plex_view_model/plex_async_action
- plex_view_model/plex_view_model
- plex_widget
- plex_widgets/loading/plex_loader_v1
- plex_widgets/loading/plex_loader_v2
- plex_widgets/loading/plex_loading_enum
- plex_widgets/plex_adv_data_table
- plex_widgets/plex_app_bar
- plex_widgets/plex_backgrounds/plex_background
- plex_widgets/plex_calendar
- plex_widgets/plex_card
- plex_widgets/plex_card_glass
- plex_widgets/plex_dashboard_card
- plex_widgets/plex_dashboard_grid
- plex_widgets/plex_data_table
- plex_widgets/plex_data_table_paginated
- plex_widgets/plex_data_table_unified
- plex_widgets/plex_date_picker_widget
- plex_widgets/plex_form
- plex_widgets/plex_form_field_widgets
- plex_widgets/plex_highlight_widget
- plex_widgets/plex_info_dialog
- plex_widgets/plex_info_sheet
- plex_widgets/plex_input_widget
- plex_widgets/plex_rich_text_editor
- plex_widgets/plex_selection_list
- plex_widgets/plex_shimmer
- plex_widgets/plex_timeline
- plex_widgets/plex_wizard_form