mosaic 1.0.6 
mosaic: ^1.0.6 copied to clipboard
Mosaic is a modular architecture for Flutter that enables clean separation of features using dynamic modules, internal events, UI injection, and centralized builds
Mosaic #
Mosaic is a comprehensive Flutter architecture framework designed for building scalable, modular applications. It provides a complete ecosystem for managing large applications through independent modules with their own lifecycle, dependencies, and communication patterns.
Table of Contents #
- Architecture Overview
 - Core Components
 - Installation
 - Quick Start
 - Module Lifecycle
 - Event System
 - Navigation
 - Dependency Injection
 - UI Injection
 - Reactive State Management
 - Logging System
 - Thread Safety
 - CLI Tool Usage
 - Testing
 - Production Considerations
 - Migration Guide
 - Contributing
 - License
 - Support
 
Architecture Overview #
Mosaic implements a module-based architecture where each feature is encapsulated as an independent module with its own:
- Lifecycle Management: Initialize, activate, suspend, resume, and dispose modules
 - Dependency Injection: Isolated DI containers per module
 - Event System: Type-safe inter-module communication
 - Navigation Stack: Internal navigation within modules
 - UI Injection: Dynamic UI component injection across modules
 
graph TB
    A[Application] --> B[Module Manager]
    B --> C[Module A]
    B --> D[Module B]
    B --> E[Module C]
    
    C --> F[DI Container A]
    C --> G[Navigation Stack A]
    C --> H[Event System]
    
    D --> I[DI Container B]
    D --> J[Navigation Stack B]
    D --> H
    
    E --> K[DI Container C]
    E --> L[Navigation Stack C]
    E --> H
    
    H --> M[Global Events]
    H --> N[UI Injector]
    H --> O[Logger]
Core Components #
Module System #
Modules are the fundamental building blocks of a Mosaic application. Each module represents a feature or domain with complete isolation from other modules.
Event-Driven Communication #
A robust event system enables decoupled communication between modules using channels, wildcards, and type-safe data exchange.
Dynamic UI Injection #
Inject UI components from one module into designated areas of another module without creating tight coupling.
Comprehensive Logging #
Multi-dispatcher logging system supporting console, file, and custom outputs with tag-based filtering and automatic rate limiting.
Thread Safety #
Built-in concurrency primitives including mutexes, semaphores, and automatic retry queues for safe multi-threaded operations.
Installation #
Add Mosaic to your pubspec.yaml:
dependencies:
  mosaic: ^1.0.1
Install the CLI tool globally:
dart pub global activate mosaic
Quick Start #
1. Create a New Project #
mosaic init my_app
cd my_app
2. Add Modules (Tesserae) #
mosaic tessera add user
mosaic tessera add dashboard
mosaic tessera add settings
3. Define Module Implementation #
// lib/user/user.dart
import 'package:flutter/material.dart';
import 'package:mosaic/mosaic.dart';
class UserModule extends Module {
  UserModule() : super(name: 'user');
  @override
  Widget build(BuildContext context) {
    return UserHomePage();
  }
  @override
  Future<void> onInit() async {
    // Initialize services
    di.put<UserRepository>(UserRepositoryImpl());
    di.put<AuthService>(AuthServiceImpl());
    
    // Setup event listeners
    events.on<String>('auth/logout', _handleLogout);
  }
  void _handleLogout(EventContext<String> context) {
    // Handle logout logic
    clear(); // Clear navigation stack
  }
}
class UserHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User Profile')),
      body: Column(
        children: [
          Text('Welcome to User Module'),
          ElevatedButton(
            onPressed: () => context.go('dashboard'),
            child: Text('Go to Dashboard'),
          ),
        ],
      ),
    );
  }
}
final module = UserModule();
4. Configure Application #
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:mosaic/mosaic.dart';
import 'init.dart'; // Generated by CLI
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize logger
  await mosaic.logger.init(
    tags: ['app', 'navigation', 'events'],
    dispatchers: [
      ConsoleDispatcher(),
      FileLoggerDispatcher(path: 'logs'),
    ],
  );
  // Initialize modules
  await init();
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Mosaic App',
      home: MosaicScope(),
    );
  }
}
Module Lifecycle #
Modules follow a comprehensive lifecycle with automatic state management:
stateDiagram-v2
    [*] --> Uninitialized
    Uninitialized --> Initializing: initialize()
    Initializing --> Active: onInit() success
    Initializing --> Error: onInit() fails
    Active --> Suspended: suspend()
    Suspended --> Active: resume()
    Active --> Disposing: dispose()
    Error --> Uninitialized: recover()
    Disposing --> Disposed
    Disposed --> [*]
Lifecycle Hooks #
class MyModule extends Module {
  @override
  Future<void> onInit() async {
    // Called during initialization
    // Setup services, load configuration
  }
  @override
  void onActive(RouteTransitionContext ctx) {
    // Called when module becomes active
    // Start background tasks, refresh data
  }
  @override
  Future<void> onSuspend() async {
    // Called when module is suspended
    // Pause operations, save state
  }
  @override
  Future<void> onDispose() async {
    // Called during cleanup
    // Release resources, save persistent state
  }
  @override
  Future<bool> onRecover() async {
    // Called to recover from error state
    return true; // Return false to prevent recovery
  }
}
Event System #
Basic Events #
// Listen to events
events.on<UserData>('user/profile/updated', (context) {
  print('Profile updated: ${context.data?.name}');
});
// Emit events
events.emit<UserData>('user/profile/updated', userData);
Wildcard Patterns #
// Listen to any user event
events.on<dynamic>('user/*', (context) {
  print('User event: ${context.name}');
  // context.params contains matched segments
});
// Listen to all nested events
events.on<dynamic>('user/profile/#', (context) {
  print('Profile event: ${context.params}');
});
Event Chains with Segments #
class UserEvents extends Segment {
  UserEvents() : super('user');
}
final userEvents = UserEvents();
// Chain event paths
userEvents.$('profile').$('avatar').emit<String>('new_avatar.jpg');
// Listen to chained events
userEvents.$('profile').$('*').on<dynamic>((context) {
  print('Profile event: ${context.params[0]}');
});
Retained Events #
// Emit retained event
events.emit<bool>('app/ready', true, true);
// Late subscribers will receive the retained event
events.on<bool>('app/ready', (context) {
  print('App is ready: ${context.data}');
});
Navigation #
Inter-Module Navigation #
// Navigate between modules
await router.go<String>('dashboard');
await router.go<UserData>('user', currentUser);
// Go back to previous module
router.goBack<String>('Operation completed');
Intra-Module Navigation #
// Push page within current module
final result = await router.push<String>(EditProfilePage());
// Pop from module stack
router.pop<String>('Profile saved');
// Clear module stack
router.clear();
Navigation Extensions #
// Context extensions for convenient navigation
ElevatedButton(
  onPressed: () => context.go('settings'),
  child: Text('Settings'),
)
ElevatedButton(
  onPressed: () async {
    final result = await context.push<bool>(EditPage());
    if (result == true) {
      // Handle success
    }
  },
  child: Text('Edit'),
)
Dependency Injection #
Each module has its own isolated DI container:
class MyModule extends Module {
  @override
  Future<void> onInit() async {
    // Singleton instance
    di.put<DatabaseService>(DatabaseServiceImpl());
    
    // Factory (new instance each time)
    di.factory<HttpClient>(() => HttpClient());
    
    // Lazy singleton (created on first access)
    di.lazy<ExpensiveService>(() => ExpensiveService());
  }
  void someMethod() {
    final db = di.get<DatabaseService>();
    final client = di.get<HttpClient>(); // New instance
    final expensive = di.get<ExpensiveService>(); // Created if needed
  }
}
UI Injection #
Dynamically inject UI components across modules:
// In source module - inject component
class ProfileModule extends Module {
  @override
  void onInit() {
    injector.inject(
      'dashboard/sidebar',
      ModularExtension(
        (context) => ListTile(
          leading: Icon(Icons.person),
          title: Text('Profile'),
          onTap: () => context.go('profile'),
        ),
        priority: 1,
      ),
    );
  }
}
// In target module - receive components
class DashboardSidebar extends ModularStatefulWidget {
  const DashboardSidebar({Key? key}) 
    : super(key: key, path: ['dashboard', 'sidebar']);
  @override
  ModularState<DashboardSidebar> createState() => _DashboardSidebarState();
}
class _DashboardSidebarState extends ModularState<DashboardSidebar> {
  _DashboardSidebarState() : super('sidebar');
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Dashboard Menu'),
        ...extensions.map((ext) => ext.builder(context)),
      ],
    );
  }
}
Reactive State Management #
Signals #
class CounterModule extends Module {
  final counter = Signal<int>(0);
  final user = AsyncSignal<User>(() => api.getCurrentUser());
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Watch signal changes
        counter.when(
          Text('Count: ${counter.state}'),
        ),
        
        // Handle async state
        user.then(
          success: (user) => Text('Hello ${user.name}'),
          loading: () => CircularProgressIndicator(),
          error: (error) => Text('Error: $error'),
          orElse: () => Text('No user data'),
        ),
        
        ElevatedButton(
          onPressed: () => counter.state++,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
Computed Signals #
final name = Signal<String>('John');
final age = Signal<int>(25);
// Computed signal automatically updates
final greeting = name.computed((value) => 'Hello, $value');
// Combined signals
final profile = name.combine(age, (name, age) => '$name ($age years old)');
Logging System #
Basic Logging #
class MyModule extends Module with Loggable {
  @override
  List<String> get loggerTags => ['user', 'authentication'];
  Future<void> loginUser(String email) async {
    info('Login attempt for $email'); // Auto-tagged
    
    try {
      final user = await authService.login(email);
      info('Login successful for $email');
    } catch (e) {
      error('Login failed: $e');
    }
  }
}
Logger Configuration #
await logger.init(
  tags: ['app', 'network', 'database'],
  dispatchers: [
    ConsoleDispatcher(),
    FileLoggerDispatcher(
      path: 'logs',
      fileNameRole: (tag) => '${tag}_${DateTime.now().day}.log',
    ),
  ],
);
// Add message formatters
logger.addWrapper(Logger.addData); // Timestamp
logger.addWrapper(Logger.addType); // Log level
logger.addWrapper(Logger.addTags); // Tag list
// Set log level for production
logger.setLogLevel(LogLevel.warning);
Thread Safety #
Mutex for Safe Data Access #
final userCache = Mutex<Map<String, User>>({});
// Safe read/write operations
final users = await userCache.get();
await userCache.set({...users, 'new_id': newUser});
// Safe compound operations
await userCache.use((users) async {
  users['user_id'] = await loadUser('user_id');
  await saveToDatabase(users);
});
Semaphore for Concurrency Control #
final downloadSemaphore = Semaphore(); // Allow 1 concurrent operation
Future<void> downloadFile(String url) async {
  await downloadSemaphore.lock();
  try {
    // Download file
  } finally {
    downloadSemaphore.release();
  }
}
Auto Retry Queue #
final queue = InternalAutoQueue(maxRetries: 3);
// Operations are automatically retried on failure
final result = await queue.push<String>(() async {
  final response = await http.get(unreliableEndpoint);
  if (response.statusCode != 200) throw Exception('HTTP ${response.statusCode}');
  return response.body;
});
CLI Tool Usage #
Project Management #
# Create new project
mosaic init my_project
# Show project status
mosaic status
# List all modules
mosaic tessera list
Module Management #
# Add new module
mosaic tessera add user_profile
# Enable/disable modules
mosaic tessera enable user_profile
mosaic tessera disable old_feature
# Manage dependencies
mosaic deps add user_profile authentication
mosaic deps remove user_profile old_service
Profile System #
# Create development profile
mosaic profile add development user_profile dashboard settings
# Switch profiles
mosaic profile switch development
# Run profile-specific commands
mosaic run development
mosaic build production
Code Generation #
# Generate type-safe event classes
mosaic events generate
# Sync modules and generate init files
mosaic sync
Testing #
Module Testing #
import 'package:flutter_test/flutter_test.dart';
import 'package:mosaic/mosaic.dart';
void main() {
  group('UserModule Tests', () {
    late UserModule userModule;
    late MockUserRepository mockRepo;
    setUp(() async {
      userModule = UserModule();
      mockRepo = MockUserRepository();
      
      // Override dependencies for testing
      userModule.di.override<UserRepository>(mockRepo);
      
      await userModule.initialize();
    });
    tearDown(() async {
      await userModule.dispose();
    });
    test('should load user profile on init', () async {
      when(mockRepo.getCurrentUser()).thenAnswer((_) async => testUser);
      
      await userModule.onInit();
      
      verify(mockRepo.getCurrentUser()).called(1);
    });
    test('should handle navigation stack', () async {
      final widget = ProfileEditPage();
      final future = userModule.push<String>(widget);
      
      expect(userModule.stackDepth, equals(1));
      
      userModule.pop<String>('saved');
      final result = await future;
      
      expect(result, equals('saved'));
      expect(userModule.stackDepth, equals(0));
    });
  });
}
Event System Testing #
group('Events Tests', () {
  test('should emit and receive events', () async {
    String? receivedData;
    
    events.on<String>('test/event', (context) {
      receivedData = context.data;
    });
    
    events.emit<String>('test/event', 'test data');
    
    await Future.delayed(Duration.zero);
    expect(receivedData, equals('test data'));
  });
  test('should handle wildcard patterns', () async {
    final receivedEvents = <String>[];
    
    events.on<String>('user/*/update', (context) {
      receivedEvents.add(context.params[0]);
    });
    
    events.emit<String>('user/profile/update', 'profile data');
    events.emit<String>('user/settings/update', 'settings data');
    
    await Future.delayed(Duration.zero);
    expect(receivedEvents, containsAll(['profile', 'settings']));
  });
});
Production Considerations #
Performance Optimization #
- Use specific log tags in production to reduce overhead
 - Set appropriate log levels (warning or error) for production
 - Enable rate limiting for high-traffic event channels
 - Consider lazy loading for expensive module dependencies
 
Error Handling #
- Implement comprehensive error recovery in modules
 - Use circuit breaker patterns for external service calls
 - Monitor module health status regularly
 - Set up alerting for critical module failures
 
Security #
- Validate all data passed through events
 - Sanitize user inputs in UI injection components
 - Use proper access controls for sensitive modules
 - Regular security audits of inter-module communications
 
Monitoring #
// Monitor module health
final healthStatus = mosaic.registry.getHealthStatus();
for (final entry in healthStatus.entries) {
  final moduleName = entry.key;
  final health = entry.value;
  
  if (health['hasError']) {
    alertService.notifyModuleError(moduleName, health['lastError']);
  }
}
Migration Guide #
From Existing Architecture #
- Identify Feature Boundaries: Map existing features to modules
 - Extract Dependencies: Move shared services to appropriate modules
 - Convert Navigation: Replace existing routing with Mosaic navigation
 - Update State Management: Migrate to Signals or keep existing solution
 - Add Event Communication: Replace tight coupling with events
 
Breaking Changes #
See CHANGELOG.md for detailed migration instructions between versions.
Contributing #
We welcome contributions! Please see our Contributing Guidelines for details.
Development Setup #
# Clone repository
git clone https://github.com/marcomit/mosaic.git
cd mosaic
# Install dependencies
flutter pub get
# Run tests
flutter test
# Install CLI locally
dart pub global activate --source path .
License #
This project is licensed under the BSD 3-Clause License. See LICENSE for details.
Support #
- Documentation: Mosaic Docs
 - Issues: GitHub Issues
 - Discussions: GitHub Discussions
 - Package: pub.dev