Understanding Modules

Modules are the heart of Mosaic. Think of them as independent pieces of your app that can work alone but also connect with other pieces when needed.

What is a Module?

A module is like a mini-app within your app. Each module:

  • Has its own screen(s) - what users see and interact with
  • Handles one main feature - like user profiles, settings, or a shopping cart
  • Can talk to other modules - but doesn’t need to know about them directly
  • Manages its own state - keeps track of its own data and user interactions

Real-World Example

Imagine you’re building a music app. You might have these modules:

  • Player Module - controls music playback (play, pause, skip)
  • Library Module - shows all the user’s music
  • Search Module - finds new songs and artists
  • Profile Module - user account and preferences

Each module focuses on just one thing, making your code easier to understand and maintain.

Module Lifecycle

Every module goes through stages, like a person’s life:

  1. Birth - Module is created
  2. Setup - Module prepares itself (onInit())
  3. Active - Module is being used (onActive())
  4. Inactive - Module is hidden but still alive
  5. Death - Module is destroyed and cleaned up

Understanding this lifecycle helps you know when to set things up and when to clean them up.

Creating Your First Module

Let’s build a simple profile module step by step:

Step 1: Basic Structure

import 'package:flutter/material.dart';
import 'package:mosaic/mosaic.dart';

class ProfileModule extends Module {
  // Give your module a name
  ProfileModule() : super(name: 'profile');

  @override
  Widget build(BuildContext context) {
    // This is what users see
    return Scaffold(
      appBar: AppBar(title: Text('My Profile')),
      body: Text('Profile content goes here'),
    );
  }
}

That’s it! You have a basic module that shows a screen.

Step 2: Add Initialization

class ProfileModule extends Module {
  ProfileModule() : super(name: 'profile');
  
  String? userName;  // Store user data

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My Profile')),
      body: Center(
        child: Column(
          children: [
            Text('Welcome, ${userName ?? "Guest"}!'),
            ElevatedButton(
              onPressed: () {
                // Tell other modules something happened
                events.emit<String>('profile/viewed', userName ?? 'guest');
              },
              child: Text('View Profile'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  Future<void> onInit() async {
    // This runs once when the module is set up
    logger.info('Profile module is initializing');
    
    // Listen for user login events
    events.on<String>('auth/user_logged_in', (context) {
      userName = context.data;  // Save the user's name
      logger.info('User logged in: $userName');
    });
    
    // Load any saved user data
    await loadUserData();
  }
  
  Future<void> loadUserData() async {
    // Simulate loading user data
    await Future.delayed(Duration(seconds: 1));
    logger.info('User data loaded');
  }
}

Step 3: Handle Activation

class ProfileModule extends Module {
  ProfileModule() : super(name: 'profile');
  
  String? userName;
  int viewCount = 0;  // Track how many times profile was viewed

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My Profile')),
      body: Center(
        child: Column(
          children: [
            Text('Welcome, ${userName ?? "Guest"}!'),
            Text('Profile views: $viewCount'),
            ElevatedButton(
              onPressed: () {
                events.emit<String>('profile/viewed', userName ?? 'guest');
              },
              child: Text('View Profile'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  Future<void> onInit() async {
    logger.info('Profile module is initializing');
    
    events.on<String>('auth/user_logged_in', (context) {
      userName = context.data;
    });
    
    await loadUserData();
  }

  @override
  void onActive() {
    // This runs every time user switches to this module
    viewCount++;
    logger.info('Profile module is now active (view #$viewCount)');
    
    // Tell analytics about the page view
    events.emit<Map<String, dynamic>>('analytics/page_view', {
      'page': 'profile',
      'user': userName,
      'timestamp': DateTime.now().toIso8601String(),
    });
  }
  
  Future<void> loadUserData() async {
    await Future.delayed(Duration(seconds: 1));
    logger.info('User data loaded');
  }
}

Module Communication

Modules talk to each other using events, not by calling each other directly. This keeps them independent.

Sending Messages (Emitting Events)

// In any module, you can send a message:
events.emit<String>('user/profile_updated', 'john_doe');
events.emit<bool>('settings/theme_changed', true);
events.emit<Map<String, dynamic>>('order/completed', {
  'orderId': '12345',
  'amount': 99.99,
  'items': ['coffee', 'muffin']
});

Listening for Messages

@override
Future<void> onInit() async {
  // Listen for specific events
  events.on<String>('user/profile_updated', (context) {
    String userId = context.data;
    refreshUserProfile(userId);
  });
  
  // Listen for any user-related events
  events.on<dynamic>('user/*', (context) {
    logger.info('User event happened: ${context.name}');
  });
  
  // Listen for all events everywhere (useful for logging)
  events.on<dynamic>('*/#', (context) {
    logger.debug('Event: ${context.name} with data: ${context.data}');
  });
}

Internal Navigation

Each module can have its own navigation stack - like a pile of screens within that module.

class ProfileModule extends Module {
  // ... other code ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Column(
        children: [
          Text('Main profile screen'),
          ElevatedButton(
            onPressed: () async {
              // Push a new screen onto this module's stack
              final result = await router.push<String>(EditProfileScreen());
              
              if (result != null) {
                logger.info('Profile updated: $result');
              }
            },
            child: Text('Edit Profile'),
          ),
          
          // Show any screens pushed onto the stack
          ...stack,
        ],
      ),
    );
  }
}

class EditProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            Text('Edit Profile Screen'),
            TextField(decoration: InputDecoration(labelText: 'Name')),
            Row(
              children: [
                ElevatedButton(
                  onPressed: () {
                    // Pop this screen and return data
                    router.pop<String>('Profile saved successfully');
                  },
                  child: Text('Save'),
                ),
                TextButton(
                  onPressed: () {
                    // Pop without returning data
                    router.pop();
                  },
                  child: Text('Cancel'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Module States

Modules can be in different states:

Active vs Inactive

  • Active - module is currently being shown to the user
  • Inactive - module exists but is not visible (user switched to another module)

Enabled vs Disabled

  • Enabled - module is available and can be used
  • Disabled - module is turned off (useful for feature flags)
// Check if a module is active
if (moduleManager.current?.name == 'profile') {
  print('User is viewing profile');
}

// Enable/disable modules
moduleManager.modules['experimental_feature']?.active = false;

Best Practices for Modules

1. Keep Modules Focused

Each module should do one main thing well:

// ✅ Good - focused on one feature
class ChatModule extends Module { /* handles messaging */ }
class PaymentModule extends Module { /* handles payments */ }

// ❌ Bad - doing too many things
class EverythingModule extends Module { /* handles chat, payments, profiles, etc. */ }

2. Use Clear Names

Module names should immediately tell you what they do:

// ✅ Good - clear purpose
enum ModuleEnum {
  userProfile,
  shoppingCart,
  messageCenter,
  accountSettings
}

// ❌ Bad - unclear purpose
enum ModuleEnum {
  module1,
  stuff,
  misc,
  things
}

3. Clean Up After Yourself

Always clean up when your module is destroyed:

@override
void onDestroy() {
  // Cancel any timers
  _timer?.cancel();
  
  // Close any open connections
  _websocket?.close();
  
  // Remove event listeners (Mosaic does this automatically, but you can be explicit)
  events.off('user/login', _handleLogin);
  
  super.onDestroy();
}

4. Handle Errors Gracefully

Don’t let one module crash the whole app:

@override
Future<void> onInit() async {
  try {
    await loadUserData();
  } catch (error) {
    logger.error('Failed to load user data: $error');
    // Show error state instead of crashing
    showErrorState = true;
  }
}

Testing Modules

Test your modules in isolation:

void main() {
  group('ProfileModule Tests', () {
    late ProfileModule module;
    
    setUp(() {
      module = ProfileModule();
    });
    
    test('should initialize correctly', () async {
      await module.onInit();
      expect(module.name, equals('profile'));
    });
    
    test('should handle user login event', () async {
      await module.onInit();
      
      // Simulate user login
      events.emit<String>('auth/user_logged_in', 'john_doe');
      
      // Give events time to process
      await Future.delayed(Duration.zero);
      
      expect(module.userName, equals('john_doe'));
    });
  });
}

Common Module Patterns

1. Data Loading Module

class DataModule extends Module {
  bool isLoading = false;
  String? errorMessage;
  List<dynamic> data = [];

  @override
  Future<void> onInit() async {
    await loadData();
  }
  
  Future<void> loadData() async {
    isLoading = true;
    errorMessage = null;
    
    try {
      data = await apiService.fetchData();
    } catch (error) {
      errorMessage = error.toString();
    } finally {
      isLoading = false;
    }
  }
}

2. Settings Module

class SettingsModule extends Module {
  Map<String, dynamic> settings = {};

  @override
  Future<void> onInit() async {
    settings = await loadSettings();
    
    events.on<Map<String, dynamic>>('settings/update', (context) {
      updateSetting(context.data);
    });
  }
  
  void updateSetting(Map<String, dynamic> newSetting) {
    settings.addAll(newSetting);
    saveSettings(settings);
    events.emit<Map<String, dynamic>>('settings/changed', settings);
  }
}

3. Authentication Module

class AuthModule extends Module {
  User? currentUser;
  bool isLoggedIn = false;

  Future<void> login(String username, String password) async {
    try {
      currentUser = await authService.login(username, password);
      isLoggedIn = true;
      
      events.emit<String>('auth/user_logged_in', currentUser!.id);
      router.goto(ModuleEnum.home);
    } catch (error) {
      events.emit<String>('auth/login_failed', error.toString());
    }
  }
  
  void logout() {
    currentUser = null;
    isLoggedIn = false;
    
    events.emit<void>('auth/user_logged_out', null);
    router.goto(ModuleEnum.login);
  }
}

Modules are the building blocks of your Mosaic app. Start simple, keep them focused, and use events to make them work together. As you get comfortable with basic modules, you can explore more advanced features like UI injection and complex navigation patterns.