flutter_microfrontend

pub version License: MIT Dart SDK Flutter

Build large-scale Flutter apps as independent, lazy-loadable feature modules.

flutter_microfrontend brings micro-frontend architecture principles to Flutter β€” enabling teams to split apps into fully isolated feature units that can be loaded, unloaded, and developed independently.


✨ Feature Highlights

Feature Description
πŸ”’ True Module Isolation Each module has its own DI container, state scope, and event subscriptions
⚑ Lazy Loading 6 preload strategies β€” load modules only when needed
πŸ“‘ Typed Event Bus RxDart-powered pub/sub with type safety, debounce, throttle
πŸ”Œ Plugin Architecture Analytics, Crash Reporting, Feature Flags β€” all decoupled
πŸ—ΊοΈ Modular Routing Auto-collects routes from all modules via GoRouter
πŸ§ͺ Highly Testable Every module is independently testable with mock helpers
πŸ” Dynamic Registration Register/unregister modules at runtime
πŸ›‘οΈ Error Isolation ModuleErrorBoundary prevents cascade failures
πŸ“Š Lifecycle Events Observable stream of every module state change
πŸ” Dep Graph Analysis Visualize dependencies, detect cycles at startup

πŸ“¦ Installation

dependencies:
  flutter_microfrontend: ^1.0.0
flutter pub get

πŸš€ Quick Start

1. Create a Module

import 'package:flutter_microfrontend/flutter_microfrontend.dart';
import 'package:go_router/go_router.dart';

class AuthModule extends MicroModule with RoutableModule, EventAwareModule {
  @override String get moduleId => 'auth';
  @override String get moduleName => 'Authentication';
  @override bool get isEager => true;
  @override int get loadPriority => 100;
  @override List<Type> get publishedEvents => [UserSignedInEvent];

  @override
  Future<void> onRegister(ModuleInjector injector) async {
    injector.registerLazySingleton<AuthService>(() => FirebaseAuthService());
  }

  @override
  List<RouteBase> get routes => [
    GoRoute(
      path: '/login',
      builder: (ctx, state) => LoginScreen(
        onSuccess: (userId) => emit(UserSignedInEvent(
          userId: userId, sourceModuleId: moduleId,
        )),
      ),
    ),
  ];
}

2. Bootstrap the App

void main() {
  runApp(
    MicrofrontendApp(
      title: 'My App',
      modules: [
        // Eager β€” critical modules loaded at startup
        AuthModule(),
        HomeModule(),

        // Lazy β€” loaded on demand
        DeferredModule(
          moduleId: 'shop',
          loader: () async => ShopModule(),
          preloadStrategy: PreloadStrategy.afterAppReady,
          dependencies: ['auth'],
        ),
        DeferredModule(
          moduleId: 'settings',
          loader: () async => SettingsModule(),
          preloadStrategy: PreloadStrategy.whenIdle,
        ),
      ],
      plugins: [
        FirebaseAnalyticsPlugin(),
        SentryPlugin(dsn: 'https://...'),
      ],
      sharedServices: (injector) {
        injector.registerSingleton<HttpClient>(DioClient());
        injector.registerSingleton<LocalStorage>(HiveStorage());
      },
    ),
  );
}

🧩 Module Contracts (Mixins)

Mixin Purpose
RoutableModule Declare GoRouter routes
EventAwareModule Publish and subscribe to typed events
ServiceModule Expose services to the global DI container
class ShopModule extends MicroModule
    with RoutableModule, EventAwareModule, ServiceModule {
  @override String get moduleId => 'shop';
  // ... implements all three contracts
}

πŸ“‘ Event System

// Publisher (in ShopModule)
emit(OrderPlacedEvent(orderId: 'ORD-123', sourceModuleId: moduleId));

// Subscriber (in any other module, no coupling)
on<OrderPlacedEvent>().listen((event) {
  showNotification('Order ${event.orderId} placed!');
});

// Auto-cancelled listener (tied to module lifecycle)
listen<UserSignedOutEvent>((event) => clearCart());

// Advanced: filter + debounce
eventBus.subscribe<SearchQueryEvent>(
  onEvent: runSearch,
  where: (e) => e.query.length > 2,
  debounce: const Duration(milliseconds: 300),
);

⚑ Lazy Loading

// Builder pattern
final analyticsModule = LazyModuleBuilder('analytics')
  .withName('Analytics Dashboard')
  .withLoader(() async => AnalyticsDashboardModule())
  .withDependencies(['auth'])
  .withStrategy(PreloadStrategy.whenIdle)
  .withConfig(PreloadConfig(maxRetries: 3))
  .build();

// In UI: show loading state automatically
LazyModuleWidget(
  module: shopDeferredModule,
  builder: (ctx, module) => ShopHomePage(),
  loading: const ShopSkeletonScreen(),
  error: (err, retry) => ShopErrorPage(onRetry: retry),
)

Preload Strategies

Strategy When
onDemand Only when explicitly called
onFirstNavigation When user navigates to the module
afterAppReady Shortly after all eager modules initialize
afterDelay After a configurable delay
whenIdle When the device is idle (3s after startup)
onWifiConnection When Wi-Fi is available

πŸ”Œ Plugin System

class FirebaseAnalyticsPlugin extends AnalyticsPlugin {
  @override String get pluginId => 'firebase_analytics';
  @override bool get initializeBeforeModules => true;

  @override
  Future<void> initialize(GlobalInjector injector, ModuleEventBus bus) async {
    await Firebase.initializeApp();
    injector.registerSingleton<FirebaseAnalytics>(FirebaseAnalytics.instance);
  }

  @override
  Future<void> trackScreen(String name, {String? moduleId, ...}) async {
    await FirebaseAnalytics.instance.setCurrentScreen(screenName: name);
  }
}

πŸ’‰ Dependency Injection

// In onRegister β€” module-local services
injector.registerLazySingleton<CartRepository>(
  () => CartRepositoryImpl(http: injector.get<HttpClient>()),
);

// Expose a service globally (other modules can use it)
injector.exposeGlobally<AuthService>(() => FirebaseAuthService());

// In widgets β€” access from widget tree
final cartService = ScopedLocator.of(context).get<CartRepository>();
// or with extension:
final cartService = context.read<CartRepository>();

πŸ§ͺ Testing

// Use MockModule for isolated tests
final mockAuth = MockModule(
  id: 'auth',
  moduleName: 'Auth',
  onInitCallback: () => print('Auth init!'),
);

// Use MockEventBus to verify events
final bus = MockEventBus();
bus.emit(UserSignedInEvent(userId: '1', ...));
expect(bus.eventsOfType<UserSignedInEvent>().length, equals(1));

πŸ—οΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   MicrofrontendApp                    β”‚
β”‚  (Bootstrap: registry + event bus + plugins + router) β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
      β”‚                      β”‚                    β”‚
 PluginRegistry        ModuleRegistry       ModuleRouter
  (Analytics,          (Lifecycle +          (All Routes
   Sentry, Flags)       DI + Events)          from all
                                              Modules)
      β”‚
      β”œβ”€β”€ GlobalInjector (shared services)
      β”‚
      β”œβ”€β”€ AuthModule ──── ModuleInjector (scoped)
      β”‚                   EventAwareModule (publishes/subscribes)
      β”‚
      β”œβ”€β”€ HomeModule ──── ModuleInjector (scoped)
      β”‚                   Subscribes to AuthModule events
      β”‚
      └── DeferredModule (ShopModule) ── Not loaded until needed
          DeferredModule (SettingsModule) ── Loads when idle

πŸ“ Package Structure

lib/
β”œβ”€β”€ flutter_microfrontend.dart  ← Single barrel export
└── src/
    β”œβ”€β”€ core/                   ← Registry, Loader, Lifecycle, App
    β”œβ”€β”€ contracts/              ← MicroModule + all mixins
    β”œβ”€β”€ navigation/             ← ModuleRouter, RouteRegistration
    β”œβ”€β”€ events/                 ← EventBus, ModuleEvent, EventChannel
    β”œβ”€β”€ injection/              ← ModuleInjector, GlobalInjector, ScopedLocator
    β”œβ”€β”€ lazy/                   ← DeferredModule, LazyModuleBuilder, PreloadStrategy
    β”œβ”€β”€ plugins/                ← MicroPlugin, PluginRegistry, specialized contracts
    β”œβ”€β”€ state/                  ← ModuleStateScope, SharedStateBridge
    β”œβ”€β”€ widgets/                ← ModuleBoundary, LazyModuleWidget, ErrorBoundary
    └── utils/                  ← Logger, Validator, DependencyGraph

πŸ“ Design Principles

  1. Isolation First β€” Modules cannot directly import each other
  2. Lazy by Default β€” Load only what's needed, when it's needed
  3. Explicit Contracts β€” Every module declares what it provides and needs
  4. Pluggable β€” Add/remove plugins without touching modules
  5. Observable β€” Every lifecycle transition is emitted to a stream

πŸ”— Dependencies

Package Purpose
rxdart Reactive Event Bus (PublishSubject, BehaviorSubject, operators)
get_it Scoped DI containers
go_router Modular routing
equatable Value equality for events
logging Structured logging
meta Annotations (@mustCallSuper, etc.)

πŸ“„ License

MIT β€” see LICENSE

Libraries

flutter_microfrontend
flutter_microfrontend