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()
andpop()
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();
}
Navigation Methods
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
andmoduleManager.currentModule
- Triggers
onInactive()
on old module andonActive()
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',
});
}
Navigation Events
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