API Reference

This is the complete API documentation for Mosaic. Each class, method, and property is explained with clear examples and use cases.

Core Classes

Module

The base class that all application modules extend. Represents a self-contained feature or screen section.

abstract class Module {
  bool active;
  final String name;
  final bool fullScreen;
  Iterable<Widget> get stack;
  
  Module({
    required this.name,
    this.active = true,
    this.fullScreen = false,
  });
}

Properties

active bool

  • Controls whether the module is enabled and can be used
  • When false, the module is disabled and won’t be displayed
  • Can be changed at runtime to enable/disable features
// Disable a module temporarily
moduleManager.modules['experimental']?.active = false;

// Re-enable it later
moduleManager.modules['experimental']?.active = true;

name String (required)

  • Unique identifier for the module
  • Used by the module manager and router to reference the module
  • Should be descriptive and match your module enum
class ProfileModule extends Module {
  ProfileModule() : super(name: 'profile');  // Must match ModuleEnum.profile
}

fullScreen bool

  • Controls whether the module takes up the entire screen
  • When true, removes standard app scaffolding
  • Useful for splash screens, onboarding, or immersive experiences
class SplashModule extends Module {
  SplashModule() : super(
    name: 'splash',
    fullScreen: true,  // No app bar, navigation, etc.
  );
}

stack Iterable<Widget> (read-only)

  • Contains all widgets pushed onto this module’s internal navigation stack
  • Automatically managed by push() and pop() methods
  • Include in your build() method to display stacked content
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        Text('Main module content'),
        ...stack,  // Show any pushed widgets
      ],
    ),
  );
}

Abstract Methods

build(BuildContext context) Widget

  • Required - You must implement this method
  • Returns the main UI widget for this module
  • Called when the module needs to be displayed
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('My Module')),
    body: Center(
      child: Text('Module content goes here'),
    ),
  );
}

Lifecycle Methods

onInit() Future<void>

  • Called once when the module is first set up
  • Use for initialization: event listeners, loading data, setting up services
  • Always call super.onInit() if you override this
@override
Future<void> onInit() async {
  logger.info('Initializing profile module');
  
  // Set up event listeners
  events.on<String>('user/login', _handleUserLogin);
  
  // Load initial data
  await _loadUserProfile();
  
  // Always call super
  await super.onInit();
}

onActive() void

  • Called every time the user switches to this module
  • Use for: refreshing data, resuming animations, analytics tracking
  • Does not return a Future (synchronous)
@override
void onActive() {
  logger.info('User is now viewing profile');
  
  // Track page view
  analytics.trackPageView('profile');
  
  // Refresh data if needed
  _refreshIfStale();
}

onInactive() void

  • Called when the user switches away from this module
  • Use for: pausing expensive operations, saving state
  • Module is still alive, just not visible
@override
void onInactive() {
  logger.info('Profile module is now inactive');
  
  // Pause any animations or timers
  _pausePeriodicUpdates();
  
  // Save any unsaved changes
  _saveCurrentState();
}

onDestroy() void

  • Called when the module is being permanently removed
  • Use for: cleanup, removing event listeners, disposing resources
  • Always call super.onDestroy() at the end
@override
void onDestroy() {
  logger.info('Profile module is being destroyed');
  
  // Cancel timers
  _updateTimer?.cancel();
  
  // Remove event listeners
  events.deafen(_userLoginListener);
  
  // Dispose of controllers
  _textController.dispose();
  
  // Always call super last
  super.onDestroy();
}

push<T>(Widget widget) Future<T>

  • Adds a widget to this module’s internal navigation stack
  • Returns a Future that completes when the widget is popped
  • The Future contains any data passed to pop()
void editProfile() async {
  final result = await router.push<bool>(
    EditProfileScreen(currentUser: user),
  );
  
  if (result == true) {
    _refreshUserProfile();
    showSuccessMessage('Profile updated!');
  }
}

pop<T>([T? value]) void

  • Removes the top widget from this module’s navigation stack
  • Optionally returns data to the code that called push()
  • Does nothing if the stack is empty
// In a pushed widget
void saveChanges() {
  if (_formIsValid()) {
    // Save data and return success
    _saveProfile();
    router.pop<bool>(true);
  } else {
    // Return failure
    router.pop<bool>(false);
  }
}

void cancel() {
  // Pop without returning data
  router.pop();
}

clear() void

  • Removes ALL widgets from this module’s navigation stack
  • Returns to the module’s base screen
  • Completes all pending push() Futures with null
void resetToHome() {
  router.clear();  // Remove all pushed screens
  logger.info('Cleared navigation stack');
}

Complete Module Example

class ShoppingCartModule extends Module with Loggable {
  ShoppingCartModule() : super(name: 'cart');
  
  @override
  List<String> get loggerTags => ['cart'];
  
  final List<CartItem> _items = [];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping Cart (${_items.length})'),
        actions: [
          IconButton(
            icon: Icon(Icons.clear_all),
            onPressed: _clearCart,
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _items.length,
              itemBuilder: (context, index) {
                return CartItemWidget(
                  item: _items[index],
                  onRemove: () => _removeItem(index),
                );
              },
            ),
          ),
          
          // Show any pushed screens (like checkout flow)
          ...stack,
          
          if (_items.isNotEmpty)
            CheckoutButton(
              total: _calculateTotal(),
              onPressed: _startCheckout,
            ),
        ],
      ),
    );
  }
  
  @override
  Future<void> onInit() async {
    info('Cart module initializing');
    
    // Listen for items being added from other modules
    events.on<CartItem>('cart/add_item', (context) {
      _addItem(context.data!);
    });
    
    events.on<String>('cart/remove_item', (context) {
      _removeItemById(context.data!);
    });
    
    // Load saved cart
    await _loadSavedCart();
  }
  
  @override
  void onActive() {
    info('User viewing cart');
    
    // Refresh cart from server
    _syncWithServer();
    
    // Track analytics
    events.emit<int>('analytics/cart_viewed', _items.length);
  }
  
  @override
  void onInactive() {
    info('Cart is now inactive');
    
    // Save cart state
    _saveCartToStorage();
  }
  
  @override
  void onDestroy() {
    info('Cart module destroying');
    
    // Save any unsaved changes
    _saveCartToStorage();
    
    super.onDestroy();
  }
  
  void _addItem(CartItem item) {
    _items.add(item);
    info('Added item to cart: ${item.productName}');
    events.emit<int>('cart/count_changed', _items.length);
  }
  
  void _removeItem(int index) {
    final item = _items.removeAt(index);
    info('Removed item from cart: ${item.productName}');
    events.emit<int>('cart/count_changed', _items.length);
  }
  
  void _clearCart() {
    final count = _items.length;
    _items.clear();
    info('Cleared cart: $count items removed');
    events.emit<int>('cart/count_changed', 0);
  }
  
  Future<void> _startCheckout() async {
    if (_items.isEmpty) {
      warning('Attempted checkout with empty cart');
      return;
    }
    
    final success = await router.push<bool>(
      CheckoutScreen(items: List.from(_items)),
    );
    
    if (success == true) {
      _clearCart();
      showSuccessMessage('Order placed successfully!');
    }
  }
  
  double _calculateTotal() {
    return _items.fold(0.0, (sum, item) => sum + item.totalPrice);
  }
}

ModuleManager

Singleton that manages all modules in your application. Keeps track of which modules are available and which is currently active.

class ModuleManager {
  Map<String, Module> modules = {};
  String? defaultModule;
  String? currentModule;
  Map<String, Module> get actives;
  Module? get current;
}

Properties

modules Map<String, Module>

  • Contains all registered modules, indexed by name
  • Modules must be added here before they can be used
  • Can be modified at runtime to add/remove modules
// Register modules
moduleManager.modules['home'] = HomeModule();
moduleManager.modules['profile'] = ProfileModule();
moduleManager.modules['settings'] = SettingsModule();

// Check if a module exists
if (moduleManager.modules.containsKey('admin')) {
  print('Admin module is available');
}

defaultModule String?

  • Name of the module to show when the app starts
  • Should match a key in the modules map
  • Can be null, but then you must manually set the initial module
// Set the default starting module
moduleManager.defaultModule = 'home';

// Then initialize the router
router.init(ModuleEnum.home);

currentModule String?

  • Name of the currently active module
  • Updated automatically when you use router.goto()
  • Can be null if no module is active
// Check which module is currently active
if (moduleManager.currentModule == 'profile') {
  print('User is viewing their profile');
}

actives Map<String, Module> (read-only)

  • Returns only the modules where active = true
  • Excludes disabled modules
  • Useful for getting a list of available features
// Get all enabled modules
final availableModules = moduleManager.actives;
print('Available modules: ${availableModules.keys.join(', ')}');

// Check if a specific module is enabled
final isShopEnabled = moduleManager.actives.containsKey('shop');

current Module? (read-only)

  • Returns the currently active module instance
  • Can be null if no module is set as current
  • Shortcut for modules[currentModule]
// Get the current module
final currentMod = moduleManager.current;
if (currentMod != null) {
  print('Current module: ${currentMod.name}');
  print('Stack size: ${currentMod.stack.length}');
}

Usage Example

void setupModules() {
  // Register all your modules
  moduleManager.modules['home'] = HomeModule();
  moduleManager.modules['profile'] = ProfileModule();
  moduleManager.modules['settings'] = SettingsModule();
  moduleManager.modules['shop'] = ShopModule();
  
  // Set default
  moduleManager.defaultModule = 'home';
  
  // Initialize router
  router.init(ModuleEnum.home);
  
  // Print available modules
  print('Registered modules:');
  for (final name in moduleManager.modules.keys) {
    final module = moduleManager.modules[name]!;
    print('  $name (active: ${module.active})');
  }
}

void toggleModule(String moduleName, bool enabled) {
  final module = moduleManager.modules[moduleName];
  if (module != null) {
    module.active = enabled;
    print('Module $moduleName ${enabled ? 'enabled' : 'disabled'}');
  }
}

Events

The global event system for communication between modules. Supports wildcards and type-safe messaging.

class Events {
  static String sep = "/";
  
  EventListener<T> on<T>(String channel, EventCallback<T> callback);
  void emit<T>(String channel, [T? data, bool retain = false]);
  void deafen<T>(EventListener<T> listener);
  void pop();
}

Methods

on<T>(String channel, EventCallback<T> callback) EventListener<T>

  • Registers a listener for events on the specified channel
  • Supports wildcards: * for single segment, # for multiple segments
  • Returns an EventListener that can be used to remove the listener later
// Listen to specific events
final listener = events.on<String>('user/login', (context) {
  print('User logged in: ${context.data}');
});

// Listen with wildcards
events.on<dynamic>('user/*', (context) {
  print('User event: ${context.name}');
});

events.on<dynamic>('user/#', (context) {
  print('Any user event: ${context.name}');
  print('Params: ${context.params}');
});

emit<T>(String channel, [T? data, bool retain = false]) void

  • Sends an event on the specified channel
  • Optional data can be any type
  • retain parameter is reserved for future use
// Emit simple events
events.emit<String>('user/login', 'john_doe');
events.emit<void>('app/startup', null);

// Emit complex data
events.emit<Map<String, dynamic>>('order/completed', {
  'orderId': '12345',
  'amount': 99.99,
  'items': ['coffee', 'sandwich'],
});

// Emit with type safety
events.emit<User>('user/updated', updatedUser);

deafen<T>(EventListener<T> listener) void

  • Removes a specific event listener
  • Use the EventListener returned by on()
  • Important for preventing memory leaks
// Register listener and store reference
final userListener = events.on<String>('user/login', _handleLogin);

// Later, remove the listener
events.deafen(userListener);

pop() void

  • Removes the most recently added listener
  • Less precise than deafen(), use carefully
  • Mainly for debugging or quick cleanup
events.on<String>('test/event', (context) => print('test'));
events.pop();  // Removes the listener above

Event Patterns

Exact Match

events.emit<String>('user/login', 'john');
events.on<String>('user/login', callback);  // Matches ✅
events.on<String>('user/logout', callback); // No match ❌

Single Wildcard (*)

events.emit<String>('user/login', 'data');
events.on<String>('user/*', callback);    // Matches ✅
events.on<String>('*/login', callback);   // Matches ✅
events.on<String>('user/*/extra', callback); // No match ❌

Multi Wildcard (#)

events.emit<String>('user/profile/settings/theme', 'dark');
events.on<String>('user/#', callback);            // Matches ✅
events.on<String>('user/profile/#', callback);    // Matches ✅
events.on<String>('settings/#', callback);        // No match ❌

EventContext

The context object passed to event callbacks contains:

class EventContext<T> {
  final T? data;           // The data passed to emit()
  final String name;       // Full event name/channel
  final List<String> params; // Extracted wildcard parameters
}

Using Context Data

events.on<Map<String, dynamic>>('order/status', (context) {
  final orderData = context.data!;
  final orderId = orderData['orderId'];
  final status = orderData['status'];
  
  print('Order $orderId status: $status');
  print('Event name: ${context.name}');
});

Wildcard Parameters

// Listen with wildcards
events.on<String>('user/*/profile', (context) {
  final userId = context.params[0];  // Extracted from *
  print('Profile event for user: $userId');
});

// Emit matching event
events.emit<String>('user/john_doe/profile', 'updated');
// context.params will be ['john_doe']

Complete Events Example

class EventManager {
  final List<EventListener> _listeners = [];
  
  void initializeEventSystem() {
    // User authentication events
    _listeners.add(events.on<String>('auth/login_success', _handleLoginSuccess));
    _listeners.add(events.on<String>('auth/login_failure', _handleLoginFailure));
    _listeners.add(events.on<void>('auth/logout', _handleLogout));
    
    // Navigation events
    _listeners.add(events.on<String>('nav/module_changed', _handleModuleChange));
    
    // Error handling
    _listeners.add(events.on<String>('*/error', _handleGlobalError));
    
    // Analytics (catch all user actions)
    _listeners.add(events.on<dynamic>('user/#', _trackUserAction));
  }
  
  void _handleLoginSuccess(EventContext<String> context) {
    final username = context.data!;
    logger.info('User logged in: $username');
    
    // Load user data
    events.emit<String>('data/load_user', username);
    
    // Navigate to home
    events.emit<String>('nav/goto', 'home');
    
    // Track analytics
    events.emit<Map<String, dynamic>>('analytics/login', {
      'username': username,
      'timestamp': DateTime.now().toIso8601String(),
    });
  }
  
  void _handleLoginFailure(EventContext<String> context) {
    final error = context.data!;
    logger.error('Login failed: $error');
    
    // Show error to user
    events.emit<String>('ui/show_error', 'Login failed: $error');
    
    // Track failed attempt
    events.emit<String>('analytics/login_failure', error);
  }
  
  void _handleLogout(EventContext<void> context) {
    logger.info('User logged out');
    
    // Clear user data
    events.emit<void>('data/clear_user', null);
    
    // Navigate to login
    events.emit<String>('nav/goto', 'login');
    
    // Track analytics
    events.emit<void>('analytics/logout', null);
  }
  
  void _handleModuleChange(EventContext<String> context) {
    final moduleName = context.data!;
    logger.info('Navigation: switched to $moduleName');
    
    // Track page view
    events.emit<String>('analytics/page_view', moduleName);
  }
  
  void _handleGlobalError(EventContext<String> context) {
    final errorMessage = context.data!;
    final eventName = context.name;
    
    logger.error('Global error from $eventName: $errorMessage');
    
    // Could send to crash reporting service
    // crashlytics.recordError(errorMessage, eventName);
    
    // Show user-friendly error
    events.emit<String>('ui/show_error', 'Something went wrong. Please try again.');
  }
  
  void _trackUserAction(EventContext<dynamic> context) {
    final actionName = context.name;
    final actionData = context.data;
    
    // Send to analytics service
    analytics.track(actionName, actionData);
    
    logger.debug('User action tracked: $actionName');
  }
  
  void dispose() {
    // Clean up all listeners
    for (final listener in _listeners) {
      events.deafen(listener);
    }
    _listeners.clear();
  }
}

InternalRouter

Handles navigation between modules and manages internal navigation stacks within modules.

class InternalRouter with Loggable {
  List<String> get loggerTags => ["router"];
  ModuleEnum? get current;
  
  void init(ModuleEnum defaultModule);
  Future<T> push<T>(Widget widget);
  void pop<T>([T? value]);
}

Properties

current ModuleEnum? (read-only)

  • Returns the currently active module enum
  • Can be null if router hasn’t been initialized
  • Use to check which module is currently displayed
// Check current module
if (router.current == ModuleEnum.profile) {
  print('User is viewing profile');
}

// Handle navigation based on current module
switch (router.current) {
  case ModuleEnum.home:
    showHomeContextMenu();
    break;
  case ModuleEnum.settings:
    showSettingsHelp();
    break;
  default:
    showGenericHelp();
}

Methods

init(ModuleEnum defaultModule) void

  • Initializes the router with a starting module
  • Must be called before using other router methods
  • Usually called once in main.dart after setting up modules
void main() async {
  // Set up modules first
  moduleManager.modules['home'] = HomeModule();
  moduleManager.modules['profile'] = ProfileModule();
  moduleManager.defaultModule = 'home';
  
  // Initialize router
  router.init(ModuleEnum.home);
  
  runApp(MyApp());
}

goto(ModuleEnum module) void

  • Switches to a different module
  • Updates router.current and moduleManager.currentModule
  • Triggers onInactive() on old module and onActive() on new module
// Navigate to different modules
void goToProfile() {
  router.goto(ModuleEnum.profile);
}

void goToSettings() {
  router.goto(ModuleEnum.settings);
}

// Navigate based on conditions
void navigateBasedOnUserRole(UserRole role) {
  switch (role) {
    case UserRole.admin:
      router.goto(ModuleEnum.admin);
      break;
    case UserRole.user:
      router.goto(ModuleEnum.home);
      break;
    case UserRole.guest:
      router.goto(ModuleEnum.login);
      break;
  }
}

push<T>(Widget widget) Future<T>

  • Adds a widget to the current module’s internal navigation stack
  • Returns a Future that completes when the widget is popped
  • The Future receives any data passed to pop()
// Push and wait for result
Future<void> editUserProfile() async {
  final success = await router.push<bool>(
    EditProfileScreen(user: currentUser),
  );
  
  if (success == true) {
    _refreshProfile();
    showSuccessMessage('Profile updated!');
  }
}

// Push without waiting for result
void showHelp() {
  router.push(HelpScreen());
}

// Push with complex return data
Future<void> selectOptions() async {
  final options = await router.push<Map<String, dynamic>>(
    OptionsScreen(currentOptions: userOptions),
  );
  
  if (options != null) {
    _updateUserOptions(options);
  }
}

pop<T>([T? value]) void

  • Removes the top widget from the current module’s navigation stack
  • Optionally returns data to the code that called push()
  • Does nothing if the current module’s stack is empty
// Pop without returning data
void cancel() {
  router.pop();
}

// Pop and return data
void save() {
  if (_validateForm()) {
    _saveData();
    router.pop<bool>(true);  // Return success
  } else {
    router.pop<bool>(false); // Return failure
  }
}

// Pop with complex data
void selectItem(Item item) {
  router.pop<Map<String, dynamic>>({
    'selectedItem': item.toMap(),
    'timestamp': DateTime.now().toIso8601String(),
    'userAction': 'manual_selection',
  });
}

The router emits events when navigation occurs:

// Listen for navigation events
events.on<String>('router/push', (context) {
  print('Screen pushed to stack');
});

events.on<int>('router/pop', (context) {
  final remainingScreens = context.data!;
  print('Screen popped, $remainingScreens remaining');
});

Complete Router Example

class NavigationService {
  static void setupNavigation() {
    // Listen for navigation events
    events.on<String>('nav/goto', _handleGotoEvent);
    events.on<Map<String, dynamic>>('nav/push', _handlePushEvent);
    events.on<void>('nav/pop', _handlePopEvent);
    
    // Track navigation for analytics
    events.on<String>('router/push', (context) {
      analytics.trackEvent('screen_pushed');
    });
    
    events.on<int>('router/pop', (context) {
      analytics.trackEvent('screen_popped');
    });
  }
  
  static void _handleGotoEvent(EventContext<String> context) {
    final moduleName = context.data!;
    final moduleEnum = ModuleEnum.values.firstWhere(
      (e) => e.name == moduleName,
      orElse: () => ModuleEnum.home,
    );
    
    router.goto(moduleEnum);
    logger.info('Navigation: switched to $moduleName');
  }
  
  static void _handlePushEvent(EventContext<Map<String, dynamic>> context) {
    final data = context.data!;
    final screenType = data['type'] as String;
    final params = data['params'] as Map<String, dynamic>? ?? {};
    
    final screen = _createScreen(screenType, params);
    if (screen != null) {
      router.push(screen);
    }
  }
  
  static void _handlePopEvent(EventContext<void> context) {
    router.pop();
  }
  
  static Widget? _createScreen(String type, Map<String, dynamic> params) {
    switch (type) {
      case 'edit_profile':
        return EditProfileScreen(userId: params['userId']);
      case 'product_detail':
        return ProductDetailScreen(productId: params['productId']);
      case 'settings':
        return SettingsScreen(category: params['category']);
      default:
        logger.warning('Unknown screen type: $type');
        return null;
    }
  }
}

// Usage
class SomeModule extends Module {
  void editProfile() {
    // Direct navigation
    router.goto(ModuleEnum.profile);
  }
  
  void showProductDetail(String productId) {
    // Event-based navigation
    events.emit<Map<String, dynamic>>('nav/push', {
      'type': 'product_detail',
      'params': {'productId': productId},
    });
  }
  
  Future<void> editCurrentUser() async {
    // Direct push with result
    final success = await router.push<bool>(
      EditProfileScreen(userId: currentUser.id),
    );
    
    if (success == true) {
      showSuccessMessage('Profile updated!');
    }
  }
}

Logger

Multi-tag logging system with support for different output dispatchers.

class Logger {
  Future<void> init({
    required List<String> tags,
    required List<LoggerDispatcher> dispatchers,
    List<String>? defaultTags,
  });
  
  Future<void> debug(String message, [List<String> tags = const []]);
  Future<void> info(String message, [List<String> tags = const []]);
  Future<void> warning(String message, [List<String> tags = const []]);
  Future<void> error(String message, [List<String> tags = const []]);
}

Initialization

init({required List<String> tags, required List<LoggerDispatcher> dispatchers, List<String>? defaultTags}) Future<void>

  • Sets up the logging system with allowed tags and output methods
  • Must be called before using the logger
  • Tags filter which messages are actually logged
await logger.init(
  tags: ['app', 'network', 'ui', 'auth'],  // Only these tags will be logged
  dispatchers: [
    ConsoleDispatcher(),  // Logs to development console
    FileLoggerDispatcher(path: 'logs'),  // Logs to files
  ],
  defaultTags: ['app'],  // Added to every log message
);

Logging Methods

debug(String message, [List<String> tags]) Future<void>

  • Logs detailed debugging information
  • Typically only shown in development builds
  • Use for tracing code execution
logger.debug('Checking user authentication status', ['auth']);
logger.debug('API response: ${response.data}', ['network', 'api']);

info(String message, [List<String> tags]) Future<void>

  • Logs general information about app operation
  • Suitable for production logging
  • Use for important events and state changes
logger.info('User logged in successfully', ['auth']);
logger.info('Module loaded: ProfileModule', ['app', 'modules']);

warning(String message, [List<String> tags]) Future<void>

  • Logs potential problems that don’t stop execution
  • Use for recoverable errors or unexpected conditions
logger.warning('API request slower than expected', ['network', 'performance']);
logger.warning('Cache miss for