A modular mobile dashboard application demonstrating dynamic widget loading, shared state management, and offline-first architecture.
- Overview
- Architecture
- Features
- Framework & Libraries
- State Management
- Offline Handling
- CI/CD Strategy
- Getting Started
- Adding New Widgets
- Project Structure
Nexus Dashboard is a production-ready modular mobile application built with Flutter that showcases enterprise-level architecture patterns. The application serves as a personal systems dashboard where independent widgets (Notes, AI Chat, Analytics) can be dynamically loaded, managed, and updated without affecting the core application shell.
- Dynamic Module Loading: Widgets register themselves and load at runtime
- Offline-First Architecture: Full functionality without network connectivity
- Clean Architecture: Clear separation of concerns across layers
- State Management: BLoC pattern for predictable state handling
- Synchronization: Smart queue-based sync with conflict resolution
- Error Resilience: Module isolation prevents cascading failures
Nexus Dashboard follows Clean Architecture principles with a Modular Monolith approach, enabling independent feature development while maintaining a single deployable unit.
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Notes │ │ AI Chat │ │ Analytics │ │
│ │ Module │ │ Module │ │ Module │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┴─────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Dashboard │ │
│ │ Shell │ │
│ └──────┬──────┘ │
├──────────────────────────┼──────────────────────────────────┤
│ Domain Layer │
│ ┌────────────────┴────────────────┐ │
│ │ Business Logic & Entities │ │
│ │ Repository Interfaces │ │
│ └────────────────┬────────────────┘ │
├──────────────────────────┼──────────────────────────────────┤
│ Data Layer │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ┌──▼────┐ ┌─────▼─────┐ ┌────▼─────┐ │
│ │ Local │ │ Sync │ │ Remote │ │
│ │Storage│ │ Manager │ │ API │ │
│ │(Hive) │ │ │ │(Future) │ │
│ └───────┘ └───────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
Each feature module is self-contained with its own:
- Domain Layer: Entities and repository interfaces
- Data Layer: Models and repository implementations
- Presentation Layer: BLoC, UI pages, and module registration
Module Structure (e.g., Notes)
├── domain/
│ ├── entities/ # Business objects
│ └── repositories/ # Abstract contracts
├── data/
│ ├── models/ # Data transfer objects
│ └── repositories/ # Concrete implementations
└── presentation/
├── bloc/ # State management
├── pages/ # UI screens
└── [feature]_module.dart # Module registration
User Action → BLoC Event → Repository → Data Source
↑ ↓
└──────── BLoC State ← Repository ←─────┘
- Widgets implement
IModuleinterface - Runtime discovery via
ModuleRegistry - Hot-swappable without app restart
- Create, read, and delete notes
- Offline-first with sync option
- Timestamp tracking
- Mock conversational AI interface
- Message history persistence
- Clear chat functionality
- Dashboard statistics overview
- Activity visualization
- Storage usage tracking
- Last sync information
- Full CRUD operations without network
- Queue-based sync with retry logic
- Conflict resolution strategies
- Module-level error boundaries
- Graceful degradation
- User-friendly error messages
- Retry mechanisms
Why Flutter?
- Cross-platform efficiency: Single codebase for iOS and Android
- Native performance: Compiles to ARM machine code
- Rich ecosystem: Extensive plugin library
- Hot reload: Rapid development iteration
- Material Design: Beautiful, consistent UI out-of-the-box
- Strong typing: Dart's null safety prevents runtime errors
| Library | Version | Purpose | Reasoning |
|---|---|---|---|
flutter_bloc |
^8.1.3 | State management | Predictable, testable state with clear separation of concerns |
get_it |
^7.6.4 | Dependency injection | Loose coupling and easy testing |
hive |
^2.2.3 | Local database | Fast, lightweight NoSQL storage for offline data |
connectivity_plus |
^5.0.1 | Network detection | Monitor connectivity for sync operations |
equatable |
^2.0.5 | Value equality | Simplify state comparisons in BLoC |
uuid |
^4.1.0 | Unique IDs | Generate collision-free identifiers |
intl |
^0.18.1 | Internationalization | Date formatting and localization support |
State Management: BLoC Pattern
├── flutter_bloc: State management framework
├── equatable: Value equality for states/events
└── Provider: (via BLoC) Widget tree injection
Dependency Injection: Service Locator
└── get_it: Global service registry
Local Storage: Hive
├── hive: Core NoSQL database
└── hive_flutter: Flutter-specific initialization
Network: Connectivity Monitoring
└── connectivity_plus: Real-time network statusNexus Dashboard uses the BLoC (Business Logic Component) pattern for state management, providing:
- Clear separation between UI and business logic
- Predictable state changes via events
- Easy testing with mockable dependencies
- Scalable architecture for complex apps
┌─────────────┐
│ UI │ Dispatches events
│ (Widget) │────────────────────┐
└─────────────┘ │
│ ▼
│ ┌─────────────┐
│ Rebuilds on │ BLoC │
│ state changes │ (Business │
│ │ Logic) │
│ └─────────────┘
│ │
│ │ Calls methods
│ ▼
│ ┌─────────────┐
│ │ Repository │
│ │ (Data Layer)│
│ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────────────│Data Sources │
│(Hive, API) │
└─────────────┘
DashboardBloc
├── Manages module navigation
├── Tracks active modules
└── Coordinates module lifecycleNotesBloc, ChatBloc, AnalyticsBloc
├── Independent state per feature
├── No cross-module dependencies
└── Isolated state updatesModules communicate through:
- Shared Repository Layer: Common data access
- Event Broadcasting: Via BLoC streams
- Dashboard Coordinator: Orchestrates cross-module actions
// Example: Analytics reads from Notes/Chat repositories
class AnalyticsBloc {
final NoteRepository _noteRepo;
final ChatRepository _chatRepo;
// Aggregates data from multiple sources
Future<void> _calculateStats() async {
final notes = await _noteRepo.getNotes();
final messages = await _chatRepo.getMessages();
// Process statistics...
}
}Nexus Dashboard implements a comprehensive offline-first architecture ensuring full functionality without network connectivity.
Why Hive?
- Zero native dependencies
- Fast read/write operations
- Type-safe storage
- Minimal storage footprint
- Built-in encryption support
Storage Structure:
Hive Boxes
├── notes_box: All note documents
├── messages_box: Chat message history
├── analytics_box: Cached statistics
└── sync_queue_box: Pending sync operations
Cache-First Approach:
Read Operation Flow:
1. Check local cache (Hive)
2. Return cached data if available
3. Fetch from remote if cache miss
4. Update cache with remote data
5. Return data to UI
Write Operation Flow:
1. Update local cache immediately (Optimistic Update)
2. Add to sync queue
3. Notify UI of local change
4. Sync to remote when online
5. Resolve conflicts if any
Sync Manager Architecture:
SyncManager Components:
├── Connectivity Monitor: Detects network state changes
├── Sync Queue: Stores pending operations
├── Retry Logic: Exponential backoff for failed syncs
└── Conflict Resolver: Last-write-wins strategySync Queue Structure:
{
"type": "save_note",
"data": { /* operation data */ },
"timestamp": "2025-11-09T10:30:00Z",
"retries": 0,
"maxRetries": 3
}Synchronization Flow:
[Offline] User creates note
↓
Save to local Hive storage
↓
Add to sync queue
↓
Show success to user (optimistic UI)
↓
[Network restored]
↓
Sync Manager detects connectivity
↓
Process sync queue (FIFO)
↓
For each queued item:
├─ Attempt sync to remote
├─ On success: Remove from queue
└─ On failure: Increment retry count
└─ If retries < max: Keep in queue
└─ If retries ≥ max: Log error, remove
Strategy: Last-Write-Wins (LWW)
Conflict Detection:
1. Compare timestamps: local vs remote
2. If remote is newer:
└─ Overwrite local with remote data
3. If local is newer:
└─ Push local changes to remote
4. If timestamps equal:
└─ Use remote as source of truth- Eventual Consistency: All nodes converge to same state
- Local-First: Users never blocked by network issues
- Transparent Sync: Background synchronization
- User Control: Manual sync trigger available
Nexus Dashboard implements a comprehensive CI/CD pipeline using GitHub Actions for automated building
Code Push → GitHub Actions Trigger
↓
┌───────────────────────┐
│ Build & Test Stage │
│ - Lint code │
└───────────────────────┘
↓
┌───────────────────────┐
│ Build Stage │
│ - Build APK/AAB │
│ - Build iOS IPA │
│ - Sign artifacts │
└───────────────────────┘
↓
┌───────────────────────┐
│ Notification Stage │
│ - Email reports │
└───────────────────────┘
Triggers:
- Push to
mainordevelopbranch - Pull request creation
- Manual workflow dispatch
Jobs:
- Code linting with
flutter analyze
# Required
Flutter SDK: >=3.0.0
Dart SDK: >=3.0.0
# Recommended
Android Studio / VS Code
Xcode (for iOS development)
Git# 1. Clone the repository
git clone https://github.com/droidchief/Nexsus-Dashboard
cd nexus_dashboard
# 2. Install dependencies
flutter pub get
# 3. Run the app
flutter run
# Optional: Run tests
flutter test
# Optional: Check for issues
flutter doctor# No additional setup required
flutter run -d androidcd ios
pod install
cd ..
flutter run -d iphoneFollow these steps to add a new widget module to Nexus Dashboard:
lib/features/[your_widget]/
├── domain/
│ ├── entities/
│ │ └── [entity].dart
│ └── repositories/
│ └── [repository].dart
├── data/
│ ├── models/
│ │ └── [model].dart
│ └── repositories/
│ └── [repository]_impl.dart
└── presentation/
├── bloc/
│ ├── [widget]_bloc.dart
│ ├── [widget]_event.dart
│ └── [widget]_state.dart
├── pages/
│ └── [widget]_page.dart
└── [widget]_module.dart// lib/features/your_widget/presentation/your_widget_module.dart
import 'package:flutter/material.dart';
import '../../../core/interfaces/i_module.dart';
class YourWidgetModule implements IModule {
@override
String get id => 'your_widget';
@override
String get title => 'Your Widget';
@override
IconData get icon => Icons.your_icon;
@override
Color get color => const Color(0xFFYOURCOLOR);
@override
String getRoute() => '/your_widget';
@override
int get priority => 4; // Display order
@override
Widget getWidget() {
return BlocProvider(
create: (_) => getIt<YourWidgetBloc>()..add(LoadData()),
child: const YourWidgetPage(),
);
}
}// lib/features/your_widget/domain/entities/your_entity.dart
import 'package:equatable/equatable.dart';
class YourEntity extends Equatable {
final String id;
final String name;
// Add your fields
const YourEntity({
required this.id,
required this.name,
});
@override
List<Object?> get props => [id, name];
}
// lib/features/your_widget/domain/repositories/your_repository.dart
import '../entities/your_entity.dart';
abstract class YourRepository {
Future<List<YourEntity>> getItems();
Future<void> saveItem(YourEntity item);
Future<void> deleteItem(String id);
}// lib/features/your_widget/data/models/your_model.dart
import '../../domain/entities/your_entity.dart';
class YourModel extends YourEntity {
const YourModel({
required super.id,
required super.name,
});
factory YourModel.fromJson(Map<String, dynamic> json) {
return YourModel(
id: json['id'] as String,
name: json['name'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
};
}
}
// lib/features/your_widget/data/repositories/your_repository_impl.dart
import '../../../../core/storage/local_storage.dart';
import '../../../../core/storage/sync_manager.dart';
import '../../domain/entities/your_entity.dart';
import '../../domain/repositories/your_repository.dart';
import '../models/your_model.dart';
class YourRepositoryImpl implements YourRepository {
final LocalStorage _storage;
final SyncManager _syncManager;
YourRepositoryImpl(this._storage, this._syncManager);
@override
Future<List<YourEntity>> getItems() async {
// Implement storage logic
}
@override
Future<void> saveItem(YourEntity item) async {
// Save to local storage
// Queue for sync
await _syncManager.queueAction('save_item', data);
}
@override
Future<void> deleteItem(String id) async {
// Implement deletion logic
}
}// lib/features/your_widget/presentation/bloc/your_widget_event.dart
abstract class YourWidgetEvent extends Equatable {
const YourWidgetEvent();
@override
List<Object?> get props => [];
}
class LoadData extends YourWidgetEvent {}
// lib/features/your_widget/presentation/bloc/your_widget_state.dart
abstract class YourWidgetState extends Equatable {
const YourWidgetState();
@override
List<Object?> get props => [];
}
class YourWidgetInitial extends YourWidgetState {}
class YourWidgetLoaded extends YourWidgetState {
final List<YourEntity> items;
const YourWidgetLoaded(this.items);
@override
List<Object?> get props => [items];
}
// lib/features/your_widget/presentation/bloc/your_widget_bloc.dart
class YourWidgetBloc extends Bloc<YourWidgetEvent, YourWidgetState> {
final YourRepository _repository;
YourWidgetBloc(this._repository) : super(YourWidgetInitial()) {
on<LoadData>(_onLoadData);
}
Future<void> _onLoadData(LoadData event, Emitter emit) async {
try {
final items = await _repository.getItems();
emit(YourWidgetLoaded(items));
} catch (e) {
// Handle error
}
}
}// lib/features/your_widget/presentation/pages/your_widget_page.dart
class YourWidgetPage extends StatelessWidget {
const YourWidgetPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Your Widget')),
body: BlocBuilder<YourWidgetBloc, YourWidgetState>(
builder: (context, state) {
if (state is YourWidgetLoaded) {
return ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.items[index].name),
);
},
);
}
return const CircularProgressIndicator();
},
),
);
}
}// lib/core/di/injection.dart
Future<void> setupDependencies() async {
// ... existing code ...
// Add your repository
getIt.registerLazySingleton<YourRepository>(
() => YourRepositoryImpl(getIt(), getIt()),
);
// Add your BLoC
getIt.registerFactory(() => YourWidgetBloc(getIt()));
}// lib/features/dashboard/data/module_registry.dart
void registerModules() {
_modules.clear();
_modules.addAll([
NotesModule(),
ChatModule(),
AnalyticsModule(),
YourWidgetModule(), // Add your module here
]);
_modules.sort((a, b) => a.priority.compareTo(b.priority));
}# Hot reload to see your new widget
# It should appear on the dashboard automatically!
flutter runnexus_dashboard/
├── lib/
│ ├── main.dart # App entry point
│ ├── core/
│ │ ├── di/
│ │ │ └── injection.dart # Dependency injection setup
│ │ ├── interfaces/
│ │ │ └── i_module.dart # Module interface
│ │ ├── storage/
│ │ │ ├── local_storage.dart # Hive wrapper
│ │ │ └── sync_manager.dart # Sync logic
│ │ ├── theme/
│ │ │ ├── app_theme.dart # Theme configuration
│ │ │ └── app_typography.dart # Typography styles
│ │ └── widgets/
│ │ └── error_boundary.dart # Error handling
│ └── features/
│ ├── dashboard/
│ │ ├── data/
│ │ │ └── module_registry.dart # Module registration
│ │ └── presentation/
│ │ ├── bloc/ # Dashboard state
│ │ └── pages/ # Dashboard UI
│ ├── notes/ # Notes module
│ ├── chat/ # Chat module
│ └── analytics/ # Analytics module
├── test/ # Unit tests
├── integration_test/ # Integration tests
├── android/ # Android native code
├── ios/ # iOS native code
├── .github/
│ └── workflows/
│ └── ci_cd.yml # CI/CD pipeline
└── pubspec.yaml # Dependencies
- Lazy Loading: Modules loaded on-demand
- Const Constructors: Minimize widget rebuilds
- ListView.builder: Efficient list rendering
- Image Caching: Reduced memory footprint
- State Optimization: Only rebuild affected widgets
Victor Loveday
Senior Mobile Developer
GitHub • LinkedIn
- Flutter team for the amazing framework
- BLoC library maintainers
- Open source community
Built with ❤️ using Flutter