Skip to content

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

License

Notifications You must be signed in to change notification settings

marcomit/mosaic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

90 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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]
Loading

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 --> [*]
Loading

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

  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

About

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

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages