Architecture Overview
Mosaic implements a modular architecture that treats each feature as an independent, self-contained unit. This approach enables better separation of concerns, improved testability, and easier maintenance as your application grows.
Core Architecture Principles
1. Modular Separation
Each feature lives in its own module with clear boundaries:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Module A │ │ Module B │ │ Module C │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Logic │ │ │ │ Logic │ │ │ │ Logic │ │
│ │ UI │ │ │ │ UI │ │ │ │ UI │ │
│ │ Data │ │ │ │ Data │ │ │ │ Data │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ Mosaic Core │
│ • Events │
│ • Router │
│ • Injector │
│ • Logger │
└─────────────────┘
2. Event-Driven Communication
Modules communicate through a centralized event system, ensuring loose coupling:
// Module A doesn't know about Module B directly
events.emit<String>('user/updated', userId);
// Module B listens and responds
events.on<String>('user/updated', (context) {
refreshUserData(context.data);
});
3. Dynamic Composition
Modules can inject UI components into other modules at runtime:
// Profile module injects a widget into Home module
injector.inject(
'home/sidebar',
ModularExtension((context) => ProfileWidget()),
);
Module Anatomy
Every Mosaic module extends the base Module
class and implements specific lifecycle methods:
class ExampleModule extends Module {
ExampleModule() : super(name: 'example');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Example')),
body: Column(
children: [
Text('Module Content'),
...stack, // Injected widgets appear here
],
),
);
}
@override
Future<void> onInit() async {
// Initialize resources, register event listeners
events.on<String>('example/event', _handleEvent);
}
@override
void onActive() {
// Called when module becomes active
logger.info('Example module activated');
}
@override
void onInactive() {
// Called when module becomes inactive
}
@override
void onDestroy() {
// Cleanup resources, remove listeners
super.onDestroy();
}
}
Module Lifecycle
Modules follow a predictable lifecycle:
- Creation → Module instance is created
- Registration → Module is added to
moduleManager
- Initialization →
onInit()
is called - Activation →
onActive()
is called when displayed - Deactivation →
onInactive()
is called when hidden - Destruction →
onDestroy()
is called before removal
// Module lifecycle visualization
Module Created
↓
Registration (moduleManager.modules['name'] = module)
↓
onInit() - Setup listeners, initialize resources
↓
onActive() - Module becomes visible
↓
[Module is running and handling events]
↓
onInactive() - Module becomes hidden
↓
onDestroy() - Cleanup and removal
System Components
ModuleManager
The central registry for all modules:
class ModuleManager {
static final ModuleManager _instance = ModuleManager._internal();
factory ModuleManager() => _instance;
final Map<String, Module> modules = {};
String? defaultModule;
// Register a module
void register(String name, Module module) {
modules[name] = module;
}
// Get active module
Module? get activeModule => modules[currentModuleName];
}
// Usage
moduleManager.modules['home'] = HomeModule();
moduleManager.modules['profile'] = ProfileModule();
moduleManager.defaultModule = 'home';
Event System
Decoupled communication backbone:
class Events {
// Type-safe event emission
void emit<T>(String eventName, T data) {
// Notify all listeners for this event
}
// Type-safe event listening
StreamSubscription on<T>(String pattern, EventCallback<T> callback) {
// Register callback for pattern matching
}
// Remove specific listener
void off<T>(String eventName, EventCallback<T> callback) {
// Unregister callback
}
}
Event Patterns:
- Specific:
'user/login'
- Exact match - Single wildcard:
'user/*'
- Any immediate child - Multi wildcard:
'user/#'
- Any descendant
Router System
Manages module navigation and internal stacks:
class InternalRouter {
String? currentModule;
final Map<String, List<Widget>> _moduleStacks = {};
// Switch between modules
void goto(ModuleEnum module) {
currentModule = module.name;
// Notify listeners, update UI
}
// Push widget to current module's stack
Future<T?> push<T>(Widget widget) {
// Add widget to current module's internal stack
}
// Pop from current module's stack
void pop<T>([T? result]) {
// Remove top widget from stack, return result
}
}
UI Injector
Dynamic widget composition system:
class UIInjector {
final Map<String, List<ModularExtension>> _injections = {};
void inject(String path, ModularExtension extension) {
_injections.putIfAbsent(path, () => []).add(extension);
// Sort by priority
_injections[path]!.sort((a, b) => a.priority.compareTo(b.priority));
}
List<Widget> getWidgets(String path, BuildContext context) {
return _injections[path]
?.map((ext) => ext.builder(context))
.toList() ?? [];
}
}
// Extension wrapper
class ModularExtension {
final WidgetBuilder builder;
final int priority;
ModularExtension(this.builder, {this.priority = 0});
}
Application Structure
Recommended Project Layout
lib/
├── app/ # Application-level configuration
│ ├── app.dart # Main app widget with ModularApp
│ ├── modules.dart # Module registration
│ └── events.dart # Global event setup
│
├── core/ # Shared utilities and services
│ ├── enums/
│ │ └── module_enum.dart
│ ├── services/
│ │ ├── api_service.dart
│ │ └── storage_service.dart
│ └── utils/
│ └── constants.dart
│
├── modules/ # Feature modules
│ ├── auth/ # Authentication module
│ │ ├── auth_module.dart
│ │ ├── pages/
│ │ │ ├── login_page.dart
│ │ │ └── register_page.dart
│ │ ├── widgets/
│ │ │ └── auth_form.dart
│ │ └── services/
│ │ └── auth_service.dart
│ │
│ ├── home/ # Home module
│ │ ├── home_module.dart
│ │ ├── pages/
│ │ └── widgets/
│ │
│ └── profile/ # Profile module
│ ├── profile_module.dart
│ └── pages/
│
└── main.dart # App entry point
Module Organization
Each module should be self-contained:
// modules/shopping/shopping_module.dart
class ShoppingModule extends Module {
ShoppingModule() : super(name: 'shopping');
// Private services (dependency injection)
late final CartService _cartService;
late final PaymentService _paymentService;
@override
Future<void> onInit() async {
// Initialize services
_cartService = CartService();
_paymentService = PaymentService();
// Register event listeners
_registerEventListeners();
// Inject UI components into other modules
_registerUIInjections();
}
void _registerEventListeners() {
events.on<Product>('shopping/add_to_cart', (context) {
_cartService.addItem(context.data);
events.emit<int>('cart/updated', _cartService.itemCount);
});
events.on<PaymentInfo>('shopping/process_payment', (context) {
_paymentService.processPayment(context.data);
});
}
void _registerUIInjections() {
// Inject cart widget into navigation bar
injector.inject(
'navigation/actions',
ModularExtension(
(context) => CartIconWidget(
itemCount: _cartService.itemCount,
onTap: () => router.goto(ModuleEnum.shopping),
),
priority: 10,
),
);
}
}
Communication Patterns
1. Event-Based Communication
Scenario: User logs in, multiple modules need to update
// Auth module emits login event
class AuthModule extends Module {
void handleLoginSuccess(User user) {
events.emit<User>('auth/login_success', user);
}
}
// Profile module listens and updates
class ProfileModule extends Module {
@override
Future<void> onInit() async {
events.on<User>('auth/login_success', (context) {
_updateUserProfile(context.data);
});
}
}
// Analytics module tracks
class AnalyticsModule extends Module {
@override
Future<void> onInit() async {
events.on<User>('auth/login_success', (context) {
analytics.trackEvent('user_login', {'user_id': context.data.id});
});
}
}
2. Service-Based Communication
Scenario: Shared services across modules
// Shared service
class UserService {
static final UserService _instance = UserService._internal();
factory UserService() => _instance;
User? _currentUser;
void updateUser(User user) {
_currentUser = user;
events.emit<User>('user/updated', user);
}
User? get currentUser => _currentUser;
}
// Modules access the service
class ProfileModule extends Module {
final UserService _userService = UserService();
void updateProfile(Map<String, dynamic> profileData) {
final updatedUser = _currentUser.copyWith(profileData);
_userService.updateUser(updatedUser);
}
}
3. State Synchronization
Scenario: Keeping state in sync across modules
class StateManager {
static final Map<String, dynamic> _globalState = {};
static void setState(String key, dynamic value) {
_globalState[key] = value;
events.emit<Map<String, dynamic>>('state/updated', {
'key': key,
'value': value,
'fullState': Map.from(_globalState),
});
}
static T? getState<T>(String key) => _globalState[key] as T?;
}
// Modules listen to state changes
class ShoppingModule extends Module {
@override
Future<void> onInit() async {
events.on<Map<String, dynamic>>('state/updated', (context) {
final stateUpdate = context.data;
if (stateUpdate['key'] == 'cart_items') {
_updateCartUI(stateUpdate['value']);
}
});
}
}
Advanced Patterns
Module Factories
For complex module initialization:
class ModuleFactory {
static Module createModule(ModuleEnum type, Map<String, dynamic> config) {
switch (type) {
case ModuleEnum.home:
return HomeModule();
case ModuleEnum.shopping:
return ShoppingModule(
apiKey: config['api_key'],
environment: config['environment'],
);
case ModuleEnum.profile:
return ProfileModule(
userService: config['user_service'],
);
default:
throw Exception('Unknown module type: $type');
}
}
}
Lazy Module Loading
Load modules only when needed:
class LazyModuleManager {
final Map<String, Module Function()> _moduleFactories = {};
final Map<String, Module> _loadedModules = {};
void registerFactory(String name, Module Function() factory) {
_moduleFactories[name] = factory;
}
Future<Module> getModule(String name) async {
if (!_loadedModules.containsKey(name)) {
final factory = _moduleFactories[name];
if (factory != null) {
final module = factory();
await module.onInit();
_loadedModules[name] = module;
}
}
return _loadedModules[name]!;
}
}
Module Dependencies
Handle module dependencies:
class DependencyManager {
final Map<String, List<String>> _dependencies = {};
void addDependency(String module, String dependsOn) {
_dependencies.putIfAbsent(module, () => []).add(dependsOn);
}
Future<void> initializeWithDependencies(String moduleName) async {
final deps = _dependencies[moduleName] ?? [];
// Initialize dependencies first
for (final dep in deps) {
await initializeWithDependencies(dep);
}
// Then initialize the module
final module = moduleManager.modules[moduleName];
if (module != null && !module.initialized) {
await module.onInit();
}
}
}
Performance Considerations
Memory Management
class MemoryEfficientModule extends Module {
Timer? _periodicTimer;
StreamSubscription? _eventSubscription;
@override
Future<void> onInit() async {
// Use weak references for long-lived objects
_eventSubscription = events.on<String>('data/update', _handleUpdate);
}
@override
void onInactive() {
// Pause expensive operations when inactive
_periodicTimer?.cancel();
}
@override
void onActive() {
// Resume operations when active
_startPeriodicUpdates();
}
@override
void onDestroy() {
// Clean up all resources
_periodicTimer?.cancel();
_eventSubscription?.cancel();
super.onDestroy();
}
}
Event Optimization
class OptimizedEventHandler {
final Map<String, Timer> _debounceTimers = {};
void handleDebouncedEvent(String eventName, Function() handler) {
_debounceTimers[eventName]?.cancel();
_debounceTimers[eventName] = Timer(
const Duration(milliseconds: 300),
handler,
);
}
void dispose() {
for (final timer in _debounceTimers.values) {
timer.cancel();
}
_debounceTimers.clear();
}
}
Testing Strategies
Unit Testing Modules
import 'package:flutter_test/flutter_test.dart';
import 'package:mosaic/mosaic.dart';
void main() {
group('Module Tests', () {
late TestModule module;
setUp(() {
module = TestModule();
});
tearDown(() {
events.clear();
});
test('should initialize correctly', () async {
await module.onInit();
expect(module.initialized, isTrue);
});
test('should handle events', () async {
await module.onInit();
String? receivedData;
events.on<String>('test/response', (context) {
receivedData = context.data;
});
events.emit<String>('test/event', 'test data');
await Future.delayed(Duration.zero);
expect(receivedData, equals('processed: test data'));
});
});
}
class TestModule extends Module {
TestModule() : super(name: 'test');
@override
Widget build(BuildContext context) => Container();
@override
Future<void> onInit() async {
events.on<String>('test/event', (context) {
events.emit<String>('test/response', 'processed: String? receivedData;
events.on<String>('test/response', (context) {#123;context.data}');
});
}
}
Integration Testing
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Module Integration Tests', () {
testWidgets('should navigate between modules', (tester) async {
// Initialize app with modules
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// Verify home module is active
expect(find.text('Home'), findsOneWidget);
// Navigate to profile
router.goto(ModuleEnum.profile);
await tester.pumpAndSettle();
// Verify profile module is active
expect(find.text('Profile'), findsOneWidget);
});
testWidgets('should handle cross-module communication', (tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// Emit event from one module
events.emit<String>('user/login', 'testuser');
await tester.pumpAndSettle();
// Verify other modules responded
expect(find.text('Welcome, testuser'), findsOneWidget);
});
});
}
Migration Strategies
From Monolithic to Modular
Step 1: Identify Modules
// Before: Monolithic structure
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(), // Everything in one place
);
}
}
// After: Identify distinct features
enum AppModules {
authentication, // Login, register, password reset
userProfile, // Profile management, settings
contentFeed, // Posts, comments, likes
messaging, // Chat, notifications
settings, // App configuration
}
Step 2: Extract First Module
// Extract authentication as first module
class AuthModule extends Module {
AuthModule() : super(name: 'auth');
@override
Widget build(BuildContext context) {
return AuthFlow(); // Existing auth widgets
}
@override
Future<void> onInit() async {
// Move existing auth logic here
_setupAuthListeners();
}
}
Step 3: Gradual Migration
// Migrate incrementally
void main() {
// Start with mixed approach
moduleManager.modules['auth'] = AuthModule();
// Keep existing code for non-migrated features
runApp(HybridApp());
}
class HybridApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: isModularEnabled
? ModularApp() // New modular approach
: LegacyHomePage(), // Existing monolithic approach
);
}
}
From Other Architectures
From BLoC Pattern:
// Before: BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
// BLoC logic
}
// After: Mosaic Module with BLoC integration
class UserModule extends Module {
late final UserBloc _userBloc;
@override
Future<void> onInit() async {
_userBloc = UserBloc();
// Bridge BLoC events to Mosaic events
_userBloc.stream.listen((state) {
events.emit<UserState>('user/state_changed', state);
});
// Bridge Mosaic events to BLoC
events.on<UserEvent>('user/action', (context) {
_userBloc.add(context.data);
});
}
}
From Provider Pattern:
// Before: Provider
class UserProvider extends ChangeNotifier {
User? _user;
// Provider logic
}
// After: Mosaic Module with Provider integration
class UserModule extends Module {
late final UserProvider _userProvider;
@override
Future<void> onInit() async {
_userProvider = UserProvider();
// Listen to provider changes
_userProvider.addListener(() {
events.emit<User?>('user/changed', _userProvider.user);
});
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _userProvider,
child: UserInterface(),
);
}
}
Best Practices
1. Module Design Principles
Single Responsibility
// ✅ Good - focused on authentication
class AuthModule extends Module {
// Only auth-related functionality
}
// ❌ Avoid - mixed responsibilities
class AuthAndProfileModule extends Module {
// Auth AND profile functionality - too broad
}
Clear Boundaries
// ✅ Good - well-defined interface
class PaymentModule extends Module {
@override
Future<void> onInit() async {
events.on<PaymentRequest>('payment/process', _processPayment);
events.on<String>('payment/cancel', _cancelPayment);
}
void _processPayment(EventContext<PaymentRequest> context) {
// Clear input/output contract
final request = context.data;
// Process payment
events.emit<PaymentResult>('payment/result', result);
}
}
2. Event Design
Hierarchical Naming
// ✅ Good - hierarchical and descriptive
'user/profile/updated'
'payment/card/added'
'navigation/page/changed'
// ❌ Avoid - flat and ambiguous
'updated'
'changed'
'success'
Type Safety
// ✅ Good - strongly typed
events.emit<UserProfile>('user/profile/updated', userProfile);
events.on<UserProfile>('user/profile/updated', (context) {
final profile = context.data; // Type is UserProfile
});
// ❌ Avoid - untyped
events.emit('user/profile/updated', userProfile);
3. Module Dependencies
Minimize Dependencies
// ✅ Good - minimal external dependencies
class NotificationModule extends Module {
@override
Future<void> onInit() async {
// Only depends on events, not other modules directly
events.on<String>('user/login', _sendWelcomeNotification);
}
}
// ❌ Avoid - tight coupling
class NotificationModule extends Module {
final UserModule userModule; // Direct dependency
final ProfileModule profileModule; // Another dependency
}
Dependency Injection
// ✅ Good - inject dependencies
class PaymentModule extends Module {
final PaymentService _paymentService;
PaymentModule({PaymentService? paymentService})
: _paymentService = paymentService ?? PaymentService(),
super(name: 'payment');
}
// Register with DI container
final paymentModule = PaymentModule(
paymentService: MockPaymentService(), // For testing
);
4. Error Handling
Module-Level Error Handling
class RobustModule extends Module {
@override
Future<void> onInit() async {
try {
await _initializeServices();
_registerEventListeners();
} catch (error) {
logger.error('Failed to initialize module: $error', [name]);
events.emit<String>('module/initialization_failed', name);
}
}
void _registerEventListeners() {
events.on<String>('data/request', (context) {
try {
final result = processRequest(context.data);
events.emit<String>('data/response', result);
} catch (error) {
events.emit<String>('data/error', error.toString());
}
});
}
}
5. Performance Optimization
Lazy Initialization
class LazyModule extends Module {
ServiceA? _serviceA;
ServiceB? _serviceB;
ServiceA get serviceA {
_serviceA ??= ServiceA(); // Initialize when first accessed
return _serviceA!;
}
ServiceB get serviceB {
_serviceB ??= ServiceB();
return _serviceB!;
}
}
Event Batching
class OptimizedModule extends Module {
final List<String> _pendingUpdates = [];
Timer? _batchTimer;
void _handleUpdate(String update) {
_pendingUpdates.add(update);
_batchTimer?.cancel();
_batchTimer = Timer(Duration(milliseconds: 100), () {
_processBatchedUpdates(_pendingUpdates.toList());
_pendingUpdates.clear();
});
}
}
Common Pitfalls
1. Event Overuse
// ❌ Avoid - too many granular events
events.emit<String>('user/name/first/changed', firstName);
events.emit<String>('user/name/last/changed', lastName);
events.emit<String>('user/email/changed', email);
// ✅ Better - batch related changes
events.emit<UserProfile>('user/profile/updated', updatedProfile);
2. Circular Dependencies
// ❌ Avoid - circular event dependencies
class ModuleA extends Module {
@override
Future<void> onInit() async {
events.on<String>('b/response', (context) {
events.emit<String>('a/request', 'from A');
});
}
}
class ModuleB extends Module {
@override
Future<void> onInit() async {
events.on<String>('a/request', (context) {
events.emit<String>('b/response', 'from B'); // Circular!
});
}
}
3. Memory Leaks
// ❌ Avoid - not cleaning up listeners
class LeakyModule extends Module {
@override
Future<void> onInit() async {
events.on<String>('some/event', _handleEvent);
// No cleanup in onDestroy!
}
}
// ✅ Good - proper cleanup
class CleanModule extends Module {
late StreamSubscription _subscription;
@override
Future<void> onInit() async {
_subscription = events.on<String>('some/event', _handleEvent);
}
@override
void onDestroy() {
_subscription.cancel();
super.onDestroy();
}
}
Next Steps
Now that you understand Mosaic’s architecture, explore these advanced topics:
- UI Injection - Dynamic UI composition patterns
- Navigation - Advanced routing and stack management
- Testing - Comprehensive testing strategies
- Performance - Optimization techniques
- Best Practices - Production-ready patterns
The modular architecture is the foundation of scalable Flutter applications. Master these concepts, and you’ll build apps that grow gracefully with your needs! 🚀