mosaic 1.0.3 copy "mosaic: ^1.0.3" to clipboard
mosaic: ^1.0.3 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 #

Pub Version License: BSD-3-Clause Flutter Dart

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 #

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}');
});

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();
// 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 #

  1. Identify Feature Boundaries: Map existing features to modules
  2. Extract Dependencies: Move shared services to appropriate modules
  3. Convert Navigation: Replace existing routing with Mosaic navigation
  4. Update State Management: Migrate to Signals or keep existing solution
  5. 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 #

3
likes
150
points
665
downloads

Publisher

verified publishermarcomit.it

Weekly Downloads

Mosaic is a modular architecture for Flutter that enables clean separation of features using dynamic modules, internal events, UI injection, and centralized builds

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

argv, flutter, path, yaml

More

Packages that depend on mosaic