Admin Frontend Package
The admin_frontend package is the presentation layer of the CO2 Asset Management admin application. It contains all Flutter UI code, views, reusable components, and state management using Riverpod notifiers. This package works in concert with admin_composition (domain wiring) and admin (entry point and tests).
Package Purpose
Section titled “Package Purpose”The admin_frontend package provides:
- Presentation Layers: Feature-specific views organized by domain module
- State Management: Riverpod-based notifiers replacing GetX patterns
- Shared Components: Reusable UI elements across features
- Navigation: GoRouter integration with route definitions
- Layout System: Responsive layouts for desktop and mobile
- Application Services: Cross-cutting concerns and orchestration
Architecture Overview
Section titled “Architecture Overview”The admin_frontend follows Clean Architecture principles integrated with the domain-driven design:
Presentation Layer (admin_frontend)├── Feature Modules (estates, login, admin, etc.)│ ├── Views (screens)│ ├── Controllers/Notifiers (state management)│ └── Components (feature-specific UI)├── Shared Components (reusable across modules)├── Layouts (responsive structure)├── Orchestration (service coordination)└── Routes (navigation configuration) ↓Nomos Domain Layer (via admin_composition)├── Intents (user commands)├── Directives (business logic)└── Event-Sourced AggregatesDirectory Structure
Section titled “Directory Structure”Directorylib/src/
Directorypresentation_modules/ - Feature modules organized by domain
Directoryestates/ - Estate management (flagship module)
Directorysite/ - Site management with floor plans
Directorysite-floor-plan/ - Interactive floor plan editor
- …
Directoryasset/ - Asset management views
- …
Directoryestate-overview/ - Estate dashboard
- …
Directoryestate-map/ - Geographic visualization
- …
Directoryusers-access/ - Permission management
- …
Directoryestate-files/ - Document management
- …
Directorylogin/ - Authentication workflow
- …
Directoryregister/ - User registration
- …
Directoryuser-profile/ - Profile and preferences
- …
Directoryadmin/ - System administration
- …
Directoryprivacy-policy/ - GDPR/compliance
- …
Directorycomponents/ - Shared UI components
Directorynavigation_content/ - Sidebar navigation components
- …
Directorygeneric_form_input/ - Form field components
- …
- floorplan_annotation_view.dart - Floor plan annotations
- pencil_selection_view.dart - Drawing tools UI
Directorylayouts/ - Application layout structure
- layout.dart - Main authenticated layout
- auth_layout.dart - Authentication pages layout
- top_bar.dart - Top navigation bar
- left_bar.dart - Left sidebar
- right_bar.dart - Right sidebar panel
Directoryhelpers/ - Utility functions and widgets
Directoryutils/ - Formatting, validators, helpers
- …
Directorywidgets/ - Reusable utility widgets
- …
Directorylocalizations/ - Multi-language support
- …
Directoryapplication/ - Application layer services
Directoryservices/ - File handling, preferences
- …
Directoryorchestration/ - Command/query coordination
- …
Directorydomain/ - Domain interfaces and models
- …
Directoryroutes/ - GoRouter configuration
- …
Directoryconfig/ - App configuration
- …
State Management: Riverpod Migration
Section titled “State Management: Riverpod Migration”The admin_frontend is transitioning from GetX to Riverpod for state management. New code uses Riverpod notifiers while legacy GetX patterns are gradually refactored.
Riverpod Notifier Pattern
Section titled “Riverpod Notifier Pattern”The standard pattern for state management. State classes use either @freezed for new code or plain classes with copyWith() during migration:
// Option 1: New code - Freezed (immutable)@freezedclass AssetState with _$AssetState { const factory AssetState({ @Default(false) bool isLoading, TrackableAssetAggregate? currentAsset, @Default([]) List<AttachmentAggregate> attachments, }) = _AssetState;}
// Option 2: Legacy/Migration - Plain class with copyWith()class AssetState { final bool isLoading; final TrackableAssetAggregate? currentAsset; final List<AttachmentAggregate> attachments;
AssetState({ this.isLoading = false, this.currentAsset, this.attachments = const [], });
AssetState copyWith({ bool? isLoading, TrackableAssetAggregate? currentAsset, List<AttachmentAggregate>? attachments, }) { return AssetState( isLoading: isLoading ?? this.isLoading, currentAsset: currentAsset ?? this.currentAsset, attachments: attachments ?? this.attachments, ); }}
// Notifier with @riverpod annotation@riverpodclass AssetNotifier extends _$AssetNotifier { @override AssetState build({ required EstateId estateId, required SiteId siteId, required String assetId, }) { // Setup subscriptions and initialize state _setupAssetSubscription(); return AssetState(isLoading: true); }
// Reactive subscriptions void _setupAssetSubscription() { final subscription = ref.read(nomosAppProvider).watch<TrackableAssetAggregate>( EstateWorkspace(estateId).toNomos, EstateWorkspace.defaultTimeline, AggregateId(assetId), ).listen(_onAssetReceived);
ref.onDispose(() => subscription.cancel()); }
// State mutations Future<void> updateAsset(Map<String, dynamic> data) async { try { state = state.copyWith(isLoading: true);
final intent = UpdateTrackableAssetIntent( assetId: TrackableAssetId.fromString(assetId), // ... map data to intent );
await ref.read(nomosAppProvider).dispatch(intent, ctx: /*...*/); } finally { state = state.copyWith(isLoading: false); } }}Using Riverpod Notifiers in Views
Section titled “Using Riverpod Notifiers in Views”// Watch notifier state in widgetsclass AssetDetailView extends ConsumerWidget { final EstateId estateId; final SiteId siteId; final String assetId;
@override Widget build(BuildContext context, WidgetRef ref) { // Watch the notifier's state final assetState = ref.watch(assetNotifierProvider( estateId: estateId, siteId: siteId, assetId: assetId, ));
if (assetState.isLoading) { return const Center(child: CircularProgressIndicator()); }
return Column( children: [ Text(assetState.currentAsset?.name.value ?? 'Unknown'), // More UI... ], ); }}Key Riverpod Concepts Used
Section titled “Key Riverpod Concepts Used”| Concept | Purpose | Example |
|---|---|---|
| @riverpod class | State container with lifecycle | AssetNotifier manages asset state |
| WidgetRef | Access to providers in widgets | ref.watch(), ref.read() |
| ref.watch() | Reactive subscription to state | Rebuilds when state changes |
| ref.read() | One-time access to state | Getting current value without watching |
| ref.onDispose() | Cleanup when provider is destroyed | Cancel subscriptions |
Code Generation
Section titled “Code Generation”Run after modifying notifiers:
cd apps/admin_frontenddart run build_runner build --delete-conflicting-outputsThis generates .g.dart files for @riverpod annotations.
Key Presentation Modules
Section titled “Key Presentation Modules”Estate Module (Flagship)
Section titled “Estate Module (Flagship)”The most comprehensive module, managing complete estate administration:
Path: lib/src/presentation_modules/estates/
Key Sub-Modules:
-
Site Floor Plan (
site-floor-plan/) - Interactive PDF-based floor plans with:- Multi-node asset placement and editing
- Real-time collaborative drawing
- Zone and region management
- Asset drag-and-drop operations
-
Estate Overview - Dashboard with analytics and metrics
-
Site Management - Site CRUD operations and configuration
-
Asset Management - Track 25+ asset types with lifecycle
-
User Access - Estate-level permission management
-
File Management - Document organization and versioning
Key Controllers:
EstateOverviewNotifier- Dashboard state and analyticsSitesListNotifier- Site listing with search and filteringAssetNotifier- Asset detail view with attachments and commentsSiteTopologyNotifier- Floor plan topology and structure
Login Module
Section titled “Login Module”User authentication and session management:
Path: lib/src/presentation_modules/login/
Features:
- Firebase authentication integration
- Email verification workflow
- Password reset functionality
- Session persistence
Key Components:
LoginNotifier- Authentication stateEmailVerificationNotifier- Email verification processForgotPasswordNotifier- Password recovery
Admin Module
Section titled “Admin Module”System administration and configuration:
Path: lib/src/presentation_modules/admin/
Features:
- User management
- Permission configuration
- System settings
- Catalogue management
Shared Components
Section titled “Shared Components”Reusable UI components used across multiple modules:
Navigation Components
Section titled “Navigation Components”Located in lib/src/components/navigation_content/:
- TopLevelNavigationContent - Main navigation menu
- EstateSelectedLeftBarContent - Estate-specific navigation
- SiteSelectedLeftBarContent - Site-specific navigation
- AdminLeftBarContent - Admin module navigation
Form Components
Section titled “Form Components”Located in lib/src/components/generic_form_input/:
- Text input fields
- Dropdown selectors
- Date pickers
- Custom field renderers
- Validation utilities
Floor Plan Components
Section titled “Floor Plan Components”Located in lib/src/components/:
FloorplanAnnotationView- Floor plan annotation interfacePencilSelectionView- Drawing tool palettePencilCursor- Custom cursor for drawing
Utility Components
Section titled “Utility Components”NavigationHistoryWidget- Breadcrumb navigationCustomSwitch- Reusable toggle switchesCustomPopMenu- Popup menu widgetEnvironmentTag- Environment indicator
Layout System
Section titled “Layout System”The layout system provides responsive structure for the entire application:
Main Components:
-
Layout (
layout.dart) - Primary authenticated app layout- Three-panel design: left sidebar, main content, right panel
- Responsive adaptation for mobile/tablet/desktop
- Scrollable content area
-
AuthLayout (
auth_layout.dart) - Authentication pages layout- Centered, minimal layout for login/register
-
TopBar (
top_bar.dart) - Top navigation with:- Estate selection dropdown
- Global search
- Theme toggle
- Notifications panel
- User menu
-
LeftBar (
left_bar.dart) - Left sidebar with:- Route-aware navigation content
- Responsive collapse/expand
- Navigation history
-
RightBar (
right_bar.dart) - Optional right panel for details/properties
Responsive Design
Section titled “Responsive Design”The layout adapts based on screen size:
// Desktop (width > 1024px)Row( children: [ LeftBar(), // Full width sidebar Expanded( child: Column( children: [ TopBar(), // Full width top bar Expanded(child: content), ], ), ), RightBar(), // Optional details panel ],)
// Mobile (width ≤ 600px)Scaffold( appBar: MobileAppBar(), drawer: MobileDrawer(), body: content,)Domain Layer Integration
Section titled “Domain Layer Integration”The admin_frontend connects to the domain layer through dependency injection provided by admin_composition. This enables:
Query Access
Section titled “Query Access”// Watch domain aggregates via Nomosfinal assets = ref.read(nomosAppProvider).watch<TrackableAssetAggregate>( EstateWorkspace(estateId).toNomos, EstateWorkspace.defaultTimeline, aggregateId,);Command Dispatch
Section titled “Command Dispatch”// Dispatch intents to the domain layerawait ref.read(nomosAppProvider).dispatch( CreateAssetIntent( assetId: assetId, name: assetName, // ... other properties ), ctx: IntentContext( workspaceId: EstateWorkspace(estateId).toNomos, timelineId: EstateWorkspace.defaultTimeline, actorId: actorId, ),);Bounded Contexts
Section titled “Bounded Contexts”The application uses several bounded contexts:
| Context | Purpose | Aggregates |
|---|---|---|
| Estate | Property and asset management | EstateAggregate, SiteRootAggregate, TrackableAssetAggregate |
| Catalogue | Asset catalogues and listings | CatalogueAggregate, ListingAggregate |
| Permissions | Access control and roles | PermissionAggregate |
| Attachments | File and media management | AttachmentAggregate |
Application Services
Section titled “Application Services”Cross-cutting concerns handled by application services:
File Handling (application/services/):
NomosAttachmentsService- Upload/download attachments- File type validation and transformation
- Blob storage coordination
User Preferences (application/services/):
- Layout preferences (sidebar state, theme)
- Notification settings
- Search history
Navigation (via admin_composition):
- Route management through GoRouter
- Deep linking support
- Navigation state persistence
Dependencies
Section titled “Dependencies”Core dependencies defined in pubspec.yaml:
State Management
Section titled “State Management”flutter_riverpod: ^2.4.0- Reactive state managementriverpod_annotation: ^2.3.0- Code generation annotationsriverpod_generator: ^2.3.0- Code generator (dev)
UI Framework
Section titled “UI Framework”flutter- Core Flutter SDKgo_router: ^14.6.1- Navigation and routingprovider: ^6.0.5- Dependency injection
Domain Integration
Section titled “Domain Integration”admin_composition- Domain registration and wiringco2_nomos_client- Type-safe query accessintents_v1- Domain command definitionscontracts_v1- Domain aggregate definitions
Visualization
Section titled “Visualization”flutter_map: ^8.2.1- Interactive mapspdfx: ^2.9.2- PDF viewingsyncfusion_flutter_charts: ^29.1.37- Charts and analytics
Data & Storage
Section titled “Data & Storage”cloud_firestore: ^5.5.1- Real-time databasefirebase_storage: ^12.4.5- Cloud storagefirebase_auth: ^5.5.1- Authenticationhive: ^2.2.3- Local storage
Utilities
Section titled “Utilities”freezed_annotation: ^3.0.0- Immutable classes (dev:freezed: ^3.1.0)json_serializable: ^6.8.0- JSON serializationuuid: ^4.3.3- ID generationintl: ^0.20.2- Localization
Feature Workflows
Section titled “Feature Workflows”Asset Management Workflow
Section titled “Asset Management Workflow”- Load Asset - Notifier subscribes to asset aggregate
- Fetch Related Data - Estate name, site name, linked listings
- Render UI - View displays asset details
- User Edits - Form captures changes
- Save Changes - Dispatch UpdateAssetIntent to domain
- Reactive Update - Aggregate stream updates notifier
- UI Reflects Changes - Consumer widgets rebuild with new state
Site Floor Plan Workflow
Section titled “Site Floor Plan Workflow”- Initialize - Load floor plan PDF and topology
- Setup Canvas - Render floor plan with asset markers
- User Interaction - Click to select assets, drag to move
- Multi-Selection - Hold Ctrl/Cmd to select multiple
- Bulk Operations - Apply changes to selected assets
- Publish Changes - Dispatch topology updates
- Real-time Sync - Other users see changes immediately
Testing Patterns
Section titled “Testing Patterns”Unit Testing Notifiers
Section titled “Unit Testing Notifiers”test('should update asset when changes saved', () async { // Arrange final container = ProviderContainer(); final notifier = container.read(assetNotifierProvider( estateId: testEstateId, siteId: testSiteId, assetId: testAssetId, ).notifier);
// Act await notifier.updateAsset({'name': 'New Name'});
// Assert final state = container.read(assetNotifierProvider(/*...*/)); expect(state.currentAsset?.name.value, equals('New Name'));});Widget Testing Components
Section titled “Widget Testing Components”testWidgets('should render asset details', (tester) async { await tester.pumpWidget( ProviderScope( child: MaterialApp( home: AssetDetailView(/*...*/), ), ), );
expect(find.text('Asset Name'), findsOneWidget); expect(find.byType(FloatingActionButton), findsWidgets);});Development Guidelines
Section titled “Development Guidelines”- ✅ Use
@riverpodfor new state management - ✅ Keep notifiers focused on a single feature/screen
- ✅ Use
ref.watch()for reactive UI updates - ✅ Dispatch intents to domain layer via
nomosAppProvider - ✅ Subscribe to domain aggregates in
build()method - ✅ Create reusable components in
components/directory - ✅ Organize feature code in
presentation_modules/ - ✅ Use response layouts with MediaQuery breakpoints
Don’ts
Section titled “Don’ts”- ❌ Don’t mix GetX and Riverpod in new code
- ❌ Don’t put business logic in UI views
- ❌ Don’t use
ref.read()for reactive data that should be watched - ❌ Don’t create components that manage their own domain state
- ❌ Don’t directly instantiate services (use dependency injection)
- ❌ Don’t duplicate components across modules
- ❌ Don’t leave subscriptions without cleanup in
ref.onDispose()
Code Generation
Section titled “Code Generation”After modifying notifiers or state classes:
# From admin_frontend directorydart run build_runner build --delete-conflicting-outputs
# Watch mode for developmentdart run build_runner watchNavigation Structure
Section titled “Navigation Structure”Routes are defined in routes/route_names.dart and use GoRouter:
Key Routes:
/- Home/estates list/estate/:estateId- Estate overview/estate/:estateId/site/:siteId- Site details/estate/:estateId/site/:siteId/floor-plan- Floor plan editor/estate/:estateId/site/:siteId/asset/:assetId- Asset details/login- Authentication/admin- Admin panel/profile- User profile
Navigation is handled through notifiers:
ref.read(navigationNotifierProvider.notifier).toNamed( Paths.ASSET_DETAIL, parameters: { 'estateId': estateId.value, 'siteId': siteId.value, 'assetId': assetId, },);Performance Considerations
Section titled “Performance Considerations”Optimization Strategies
Section titled “Optimization Strategies”- Lazy Loading: Sub-modules load on-demand
- Pagination: Asset lists paginate large datasets
- Caching: Frequent queries cached in notifier state
- Selective Subscriptions: Only watch needed aggregates
- Debouncing: Debounce rapid form inputs before dispatch
Large Estate Optimization
Section titled “Large Estate Optimization”For estates with 1000+ assets:
- Use filtering/search to reduce visible data
- Paginate asset listings
- Lazy-load floor plan tiles
- Cache computed values in state
- Unsubscribe from unused aggregates
Common Patterns
Section titled “Common Patterns”Loading State Management
Section titled “Loading State Management”@freezedclass MyState with _$MyState { const factory MyState({ @Default(false) bool isLoading, MyData? data, String? error, }) = _MyState;}
// In notifierstate = state.copyWith(isLoading: true);try { final data = await fetchData(); state = state.copyWith(data: data, isLoading: false);} catch (e) { state = state.copyWith(error: e.toString(), isLoading: false);}Form Handling
Section titled “Form Handling”// Accumulate form state in notifiervoid updateFormField(String key, dynamic value) { state = state.copyWith( formData: {...state.formData, key: value}, );}
// Validate and saveFuture<void> submitForm() async { if (!_validateForm(state.formData)) { state = state.copyWith(error: 'Validation failed'); return; }
await saveToDatabase(state.formData);}Error Handling
Section titled “Error Handling”try { await someOperation();} catch (e) { Obs.error(Obs.nomosCommand, 'Operation failed: $e'); state = state.copyWith(error: 'Operation failed'); rethrow; // Propagate if needed}Related Documentation
Section titled “Related Documentation”- Architecture Overview - System-wide architecture
- Domain Layer - Nomos framework and event sourcing
- Admin Composition - Domain wiring and providers
- Testing Guide - Testing strategies and patterns
- Flutter Conventions - Code style and standards
Migration from GetX
Section titled “Migration from GetX”For legacy GetX code being converted to Riverpod:
- Create State Class: Use
@freezedfor immutable state - Create Notifier: Use
@riverpodclass extending_$NotifierName - Setup Subscriptions: Use
ref.onDispose()for cleanup - Update Views: Replace
GetViewwithConsumerWidget - Update Access: Change
Get.find()toref.read()/ref.watch() - Run Generator: Execute
build_runner build
Before (GetX)
Section titled “Before (GetX)”class MyController extends GetxController { final data = Rxn<MyData>();
void loadData() async { data.value = await fetchData(); }}
class MyView extends GetView<MyController> { @override Widget build(context) { return Obx(() => Text(controller.data.value?.name ?? 'Loading')); }}After (Riverpod with Freezed)
Section titled “After (Riverpod with Freezed)”@freezedclass MyState with _$MyState { const factory MyState({ MyData? data, }) = _MyState;}
@riverpodclass MyNotifier extends _$MyNotifier { @override MyState build() => const MyState();
Future<void> loadData() async { state = state.copyWith(data: await fetchData()); }}
class MyView extends ConsumerWidget { @override Widget build(context, ref) { final myState = ref.watch(myNotifierProvider); return Text(myState.data?.name ?? 'Loading'); }}After (Riverpod with Plain Class - Transition Pattern)
Section titled “After (Riverpod with Plain Class - Transition Pattern)”class MyState { final MyData? data;
MyState({this.data});
MyState copyWith({MyData? data}) { return MyState(data: data ?? this.data); }}
@riverpodclass MyNotifier extends _$MyNotifier { @override MyState build() => MyState();
Future<void> loadData() async { state = state.copyWith(data: await fetchData()); }}
class MyView extends ConsumerWidget { @override Widget build(context, ref) { final myState = ref.watch(myNotifierProvider); return Text(myState.data?.name ?? 'Loading'); }}Quick Start
Section titled “Quick Start”Creating a New Feature Module
Section titled “Creating a New Feature Module”# 1. Create directory structuremkdir -p lib/src/presentation_modules/my_feature/{controllers,views,components}
# 2. Create state class (my_feature_state.dart)@freezedclass MyFeatureState with _$MyFeatureState { const factory MyFeatureState({ @Default(false) bool isLoading, }) = _MyFeatureState;}
# 3. Create notifier (my_feature_notifier.dart)@riverpodclass MyFeatureNotifier extends _$MyFeatureNotifier { @override MyFeatureState build() => const MyFeatureState();}
# 4. Create view (my_feature_view.dart)class MyFeatureView extends ConsumerWidget { @override Widget build(context, ref) { final state = ref.watch(myFeatureNotifierProvider); return Scaffold(body: Center(child: Text('Feature'))); }}
# 5. Run code generationdart run build_runner buildAdding a New Notifier Method
Section titled “Adding a New Notifier Method”// 1. Add to notifier classFuture<void> myAction() async { try { state = state.copyWith(isLoading: true); // Do work state = state.copyWith(isLoading: false); } catch (e) { state = state.copyWith(isLoading: false); rethrow; }}
// 2. Call from viewElevatedButton( onPressed: () => ref.read(myNotifierProvider.notifier).myAction(), child: Text('Click'),)Troubleshooting
Section titled “Troubleshooting”Build Runner Not Generating Files
Section titled “Build Runner Not Generating Files”# Clean and rebuildflutter cleandart run build_runner cleandart run build_runner build --delete-conflicting-outputsRiverpod State Not Updating
Section titled “Riverpod State Not Updating”- Ensure you’re using
ref.watch()in widgets, notref.read() - Verify state is immutable (
@freezedorcopyWith()) - Check that
copyWith()returns a new instance - Verify notifier methods update state, not external variables
Memory Leaks in Subscriptions
Section titled “Memory Leaks in Subscriptions”- Always use
ref.onDispose()to cancel subscriptions - Don’t hold references to providers outside of widgets
- Use
ProviderContainerfor testing, not global instances
Navigation Issues
Section titled “Navigation Issues”- Ensure
GoRouteris properly configured in app - Use named routes from
Pathsconstants - Verify parameters match route definitions
- Check that route is added to
GoRouterconfiguration