tool_modularity 0.0.4 copy "tool_modularity: ^0.0.4" to clipboard
tool_modularity: ^0.0.4 copied to clipboard

Contains utils

tool_modularity #

A lightweight framework for building modular Flutter applications with dependency injection and localization support.

Overview #

tool_modularity provides base classes for creating independent, self-contained feature modules with their own dependencies, configuration, and lifecycle management. It's built on top of GetIt for dependency injection and supports Flutter's localization system.

Architecture #

Core Concepts #

  1. BaseModule: Abstract class that all feature modules extend
  2. Params Pattern: Configuration objects passed to modules during initialization
  3. Module Lifecycle: setupDependencies()dependenciesCreated()clear()
  4. Lazy Initialization: Modules register params, dependencies are set up on first access

Creating a New Feature Module #

1. Create Params Class #

Define configuration that your module needs from the app layer:

// lib/declaration/feature_params.dart
class FeatureParams {
  // Use Function() for lazy evaluation
  final String Function() apiHost;

  // Use FactoryFunc<T> for dependency factories
  final FactoryFunc<SomeRepository> repositoryFactory;

  // Use direct values for simple config
  final bool enableFeature;

  const FeatureParams({
    required this.apiHost,
    required this.repositoryFactory,
    this.enableFeature = true,
  });
}

Params Guidelines:

  • Use Function() callbacks for values that might change or depend on runtime state
  • Use FactoryFunc<T> (from GetIt) for providing dependencies from parent modules
  • Keep params immutable (all fields final)
  • Provide sensible defaults where possible

2. Create Module Class #

// lib/declaration/feature_module.dart
import 'package:get_it/get_it.dart';
import 'package:tool_modularity/modularity.dart';

part 'feature_module_impl.dart';

abstract class FeatureModule extends BaseModule {
  static final FeatureModule _instance = _FeatureModuleImpl();

  static FeatureModule newInstance({required FeatureParams params}) {
    if (!GetIt.instance.isRegistered<FeatureParams>()) {
      GetIt.instance.registerSingleton<FeatureParams>(params);
    }
    return _instance;
  }

  static FeatureModule get instance {
    if (!GetIt.instance.isRegistered<FeatureParams>()) {
      throw Exception('FeatureModule params not set. Call newInstance() first.');
    }
    return _instance;
  }
}

Module Pattern:

  • Singleton instance via _instance
  • newInstance() registers params and returns instance
  • instance getter validates params are registered
  • Check isRegistered before registering to support hot reload

3. Create Module Implementation (with Private DI Setup) #

// lib/declaration/feature_module_impl.dart
part of 'feature_module.dart';

class _FeatureModuleImpl extends FeatureModule {
  _FeatureModuleImpl();

  @override
  Future<void> setupDependencies(GetIt getIt) async {
    await _setupDI(getIt, getIt.get<FeatureParams>());
  }

  /// Private DI setup - not exposed outside module
  /// Keeps dependency registration internal to the module
  Future<void> _setupDI(GetIt getIt, FeatureParams params) async {
    // Data layer
    getIt.registerFactory<DataSource>(
      () => DataSourceImpl(params.apiHost()),  // Call function
    );

    // Repositories
    getIt.registerLazySingleton<MyRepository>(
      () => MyRepositoryImpl(
        dataSource: getIt.get(),
        otherDep: getIt.get(),
      ),
    );

    // Domain layer
    getIt.registerFactory<UseCase>(
      () => UseCaseImpl(getIt.get()),
    );
  }

  @override
  Future<void> dependenciesCreated(GetIt getIt) async {
    // Optional: Initialize services after all modules are set up
    await getIt.get<MyRepository>().initialize();
  }

  @override
  Future<void> clear() async {
    // Optional: Clean up resources
    await super.clear();
  }
}

Important:

  • Keep _setupDI private (prefixed with _)
  • It should only be called from setupDependencies()
  • Do NOT export DI setup functions in barrel files
  • This keeps module internals encapsulated

4. Export from Barrel File #

// lib/declaration.dart
export 'declaration/feature_module.dart';
export 'declaration/feature_params.dart';
// Note: Do NOT export feature_di.dart - keep it private to the module

Initializing Modules in the App #

In App Module #

// apps/app_game/lib/game_app_modules.dart
class GameApp extends BaseApp {
  @override
  Future<void> init(AppParams params) async {
    // Initialize feature modules with their params
    FeatureModule.newInstance(
      params: FeatureParams(
        apiHost: () => GetIt.instance.get<ConfigRepository>().getApiUrlSync(),
        repositoryFactory: () => GetIt.instance.get<SomeRepository>(),
        enableFeature: true,
      ),
    );

    return super.init(params);
  }

  @override
  List<BaseModule> get modules => [
    BaseModule.instance,
    AuthModule.instance,
    FeatureModule.instance,  // Add your module
    // ... other modules
  ];
}

Adding Localization to Modules #

Modules can provide their own localization using Flutter's l10n system. This allows feature modules to be self-contained with their own translated strings.

1. Add Localization Mixin to Module #

// lib/declaration/feature_module.dart
import 'package:feature_name/l10n/generated/feature_name_localization.dart';
import 'package:feature_name/l10n/generated/feature_name_localization_en.dart';
import 'package:flutter/widgets.dart';
import 'package:tool_modularity/modularity.dart';

abstract class FeatureModule extends BaseModule
    with LocalizationModule<FeatureNameLocalization> {  // Add mixin
  // ... rest of module code
}

// Add BuildContext extension for easy access
extension FeatureLocalizationExt on BuildContext {
  FeatureNameLocalization get featureNameLocalization =>
      FeatureNameLocalization.of(this)!;
}

2. Implement Required Localization Methods #

// lib/declaration/feature_module_impl.dart
part of 'feature_module.dart';

class _FeatureModuleImpl extends FeatureModule {
  @override
  LocalizationsDelegate get localizationsDelegate =>
      FeatureNameLocalization.delegate;

  @override
  void onLocalizationUpdate(BuildContext context) {
    updateLocalization(context.featureNameLocalization);
  }

  @override
  FeatureNameLocalization lookupLocalizations({Locale? locale}) {
    return locale?.let(lookupFeatureNameLocalization) ??
        FeatureNameLocalizationEn();
  }
}

3. Create l10n Structure #

features/feature_name/
  lib/
    l10n/
      templates/           # Translation files (ARB format)
        app_en.arb        # English translations
        app_ru.arb        # Russian translations
        app_uk.arb        # Ukrainian translations
      generated/          # Auto-generated by Flutter
        feature_name_localization.dart
        feature_name_localization_en.dart
        feature_name_localization_ru.dart
        feature_name_localization_uk.dart

4. Configure pubspec.yaml #

# pubspec.yaml
flutter:
  generate: true

# Creates l10n.yaml automatically or add manually:
# l10n.yaml
arb-dir: lib/l10n/templates
template-arb-file: app_en.arb
output-localization-file: feature_name_localization.dart
output-dir: lib/l10n/generated

5. Access Localization in Module #

In BLoC/Repository:

class MyBloc {
  final LocaleProvider localeProvider;

  String getLocalizedString() {
    final localization = FeatureModule.instance
        .localizationOrDeviseLocale(localeProvider);
    return localization.someTranslatedString;
  }

  // Or with async for app locale
  Future<String> getLocalizedStringAsync() async {
    final localization = await FeatureModule.instance
        .localizationOrAppLocale(localeProvider);
    return localization.someTranslatedString;
  }

  // Or observe changes
  Stream<String> observeLocalizedString() {
    return FeatureModule.instance
        .observeLocalization()
        .map((l) => l.someTranslatedString);
  }
}

In Widget:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Easy access via extension
    final l10n = context.featureNameLocalization;
    return Text(l10n.welcomeMessage);
  }
}

6. App Integration #

The app automatically integrates module localizations:

// In BaseApp (handled automatically)
class GameApp extends BaseApp {
  @override
  void onAppContextChange(BuildContext context) {
    // Automatically updates all LocalizationModule instances
    modules.whereType<LocalizationModule>().forEach(
      (module) => module.onLocalizationUpdate(context),
    );
    super.onAppContextChange(context);
  }
}

Example: feature_auth Localization #

// feature_auth/lib/declaration/auth_module.dart
abstract class AuthModule extends BaseModule
    with LocalizationModule<FeatureAuthLocalization> {
  // ...
}

// Usage in auth screens
class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final l10n = context.featureAuthLocalization;
    return Text(l10n.loginTitle);  // "Login" / "Вхід" / "Логин"
  }
}

Benefits:

  • Each module has its own translations
  • No coupling to app-level localization
  • Automatic language switching
  • Type-safe translation keys

Feature Toggles and Conditional Registration #

Modules can support feature toggles through params, allowing the app to enable/disable functionality at initialization time.

Pattern 1: Simple Boolean Toggle #

// Params
class FeatureParams {
  final bool enableAdvancedFeatures;

  const FeatureParams({
    this.enableAdvancedFeatures = false,
  });
}

// Module Implementation
class _FeatureModuleImpl extends FeatureModule {
  Future<void> _setupDI(GetIt getIt, FeatureParams params) async {
    // Always register core services
    getIt.registerLazySingleton<CoreService>(
      () => CoreServiceImpl(),
    );

    // Conditionally register advanced features
    if (params.enableAdvancedFeatures) {
      getIt.registerLazySingleton<AdvancedService>(
        () => AdvancedServiceImpl(getIt.get()),
      );

      getIt.registerFactory<AdvancedUseCase>(
        () => AdvancedUseCaseImpl(getIt.get()),
      );
    }
  }
}

// App initialization
FeatureModule.newInstance(
  params: FeatureParams(
    enableAdvancedFeatures: true,  // Toggle here
  ),
);

Pattern 2: Module-Specific Feature Flags with tool_config #

Recommended approach for module features that integrate with the app's feature flag system.

Example from feature_auth:

Step 1: Define Module Feature Names

// lib/config/models/auth_feature_name.dart
import 'package:tool_config/config.dart';

/// Auth module-specific feature names
/// Each feature maps to a string key used in the feature flag system
enum AuthFeatureName implements FeatureName {
  facebookAuth,
  nicknameAvailabilityCheck,
  // Add more module-specific features
}

Step 2: State Implements FeaturesStateContract

// lib/presentation/pages/login/state/login_state.dart
import 'package:tool_config/config.dart';

abstract class LoginState implements FeaturesStateContract {
  /// Declare which features this state supports
  static List<AuthFeatureName> get supportedFeatureNames => [
    AuthFeatureName.facebookAuth,
  ];

  const LoginState._();

  const factory LoginState({
    required List<Feature> features,  // From FeaturesStateContract
    // ... other fields
  }) = _LoginState;

  factory LoginState.initial() => LoginState(
    features: supportedFeatureNames
        .map((e) => Feature.enabled(e))
        .toList(),
  );
}

Step 3: Check Features in UI

// Conditional UI rendering with BlocBuilder
BlocBuilder<LoginBloc, LoginState>(
  buildWhen: (previous, current) => isFeatureChangesCondition(
    AuthFeatureName.facebookAuth,
    previous,
    current,
  ),
  builder: (context, state) {
    if (state.isFeatureEnabled(AuthFeatureName.facebookAuth)) {
      return FacebookLoginButton(
        onPressed: () => bloc.add(SignInFacebookEvent()),
      );
    }
    return const SizedBox.shrink();
  },
)

Step 4: Control Features via Remote Config

Features can be controlled at runtime via remote config:

// Features can be toggled remotely without app updates
remoteConfig.setBool('facebookAuth', false);  // Disables Facebook login

Benefits:

  • Centralized: Integrates with app-wide feature flag system
  • Remote control: Can enable/disable features without app updates
  • Type-safe: Enum ensures compile-time safety
  • Reactive: UI automatically updates when features change
  • Testable: Easy to test with different feature configurations

Module vs App Features:

  • AppFeatureName (in feature_config_api) - Global app features
  • AuthFeatureName (in feature_auth) - Module-specific features
  • Both implement FeatureName from tool_config

Pattern 3: Configuration Objects for Complex Toggles #

For complex feature toggles:

class FeatureConfig {
  final bool enableAnalytics;
  final bool enableCaching;
  final int maxCacheSize;

  const FeatureConfig({
    this.enableAnalytics = true,
    this.enableCaching = false,
    this.maxCacheSize = 100,
  });
}

class FeatureParams {
  final FeatureConfig config;

  const FeatureParams({
    required this.config,
  });
}

// Conditional registration based on config
Future<void> _setupDI(GetIt getIt, FeatureParams params) async {
  if (params.config.enableAnalytics) {
    getIt.registerLazySingleton<AnalyticsService>(
      () => AnalyticsServiceImpl(),
    );
  }

  if (params.config.enableCaching) {
    getIt.registerLazySingleton<CacheService>(
      () => CacheServiceImpl(
        maxSize: params.config.maxCacheSize,
      ),
    );
  }
}

Best Practices #

  1. Default to Safe Values: Use sensible defaults in params

    const FeatureParams({
      this.enableExperimentalFeature = false,  // Default: off
    });
    
  2. Document Feature Flags: Explain what each toggle controls

    /// Enable experimental search algorithm
    /// WARNING: Not recommended for production
    final bool enableExperimentalSearch;
    
  3. Validate Combinations: Check for invalid feature combinations

    FeatureParams({
      required this.enableFeatureA,
      required this.enableFeatureB,
    }) {
      if (enableFeatureB && !enableFeatureA) {
        throw ArgumentError('Feature B requires Feature A');
      }
    }
    
  4. Use Enums for Related Toggles: Group related features

    enum Theme { light, dark, auto }
    enum LogLevel { debug, info, warning, error }
    
  5. Keep UI in Sync: Ensure UI reflects enabled features

    // Bad: UI shows button but feature throws error
    GoogleLoginButton(
      onTap: () => throw Exception('Not enabled'),
    )
    
    // Good: Don't show button if feature is disabled
    if (state.isFeatureEnabled(AuthFeatureName.facebookAuth))
      FacebookLoginButton(onTap: _loginWithFacebook)
    

Testing with Feature Toggles #

Testing Simple Toggles

// Test conditional DI registration
void main() {
  group('With advanced features enabled', () {
    setUp(() {
      FeatureModule.newInstance(
        params: FeatureParams(enableAdvancedFeatures: true),
      );
    });

    test('should register advanced service', () {
      expect(GetIt.instance.isRegistered<AdvancedService>(), true);
    });
  });

  group('With advanced features disabled', () {
    setUp(() {
      FeatureModule.newInstance(
        params: FeatureParams(enableAdvancedFeatures: false),
      );
    });

    test('should not register advanced service', () {
      expect(GetIt.instance.isRegistered<AdvancedService>(), false);
    });
  });
}

Testing tool_config Feature Flags

// Test state with different feature configurations
void main() {
  group('LoginState with features', () {
    test('with facebook enabled', () {
      final state = LoginState(
        features: [Feature.enabled(AuthFeatureName.facebookAuth)],
      );

      expect(state.isFeatureEnabled(AuthFeatureName.facebookAuth), true);
    });

    test('with facebook disabled', () {
      final state = LoginState(
        features: [Feature.disabled(AuthFeatureName.facebookAuth)],
      );

      expect(state.isFeatureEnabled(AuthFeatureName.facebookAuth), false);
    });
  });

  group('UI responds to features', () {
    testWidgets('shows facebook button when enabled', (tester) async {
      final bloc = MockLoginBloc();
      when(() => bloc.state).thenReturn(
        LoginState(
          features: [Feature.enabled(AuthFeatureName.facebookAuth)],
        ),
      );

      await tester.pumpWidget(LoginScreen(bloc: bloc));

      expect(find.byType(FacebookLoginButton), findsOneWidget);
    });

    testWidgets('hides facebook button when disabled', (tester) async {
      final bloc = MockLoginBloc();
      when(() => bloc.state).thenReturn(
        LoginState(
          features: [Feature.disabled(AuthFeatureName.facebookAuth)],
        ),
      );

      await tester.pumpWidget(LoginScreen(bloc: bloc));

      expect(find.byType(FacebookLoginButton), findsNothing);
    });
  });
}

Common Patterns #

Pattern 1: Simple Value Parameter #

// Params
final bool enableFeature;

// Usage
FeatureParams(enableFeature: true)

Pattern 2: Lazy-Evaluated Parameter #

// Params - use Function() for runtime values
final String Function() apiHost;

// Usage
FeatureParams(
  apiHost: () => getIt.get<ConfigRepository>().getApiUrlSync(),
)

// In DI
getIt.registerLazySingleton<MyRepo>(
  () => MyRepoImpl(params.apiHost()),  // Call the function
);

Pattern 3: Dependency Factory #

// Params
final FactoryFunc<SomeRepository> repositoryFactory;

// Usage
FeatureParams(
  repositoryFactory: () => GetIt.instance.get<SomeRepository>(),
)

// In DI
getIt.registerLazySingleton<MyService>(
  () => MyServiceImpl(params.repositoryFactory()),
);

Pattern 4: Optional Dependencies #

// Params
final FactoryFunc<AnalyticsRepository>? analyticsFactory;

// In DI
if (params.analyticsFactory != null) {
  getIt.registerLazySingleton<AnalyticsService>(
    () => AnalyticsServiceImpl(params.analyticsFactory!()),
  );
}

Pattern 5: Error Code Mapper #

Modules define semantic error types and receive a mapper interface via params. The app layer provides the actual server code mappings.

Implementation

1. Define semantic error types:

// lib/data/errors/chat_error_type.dart
enum ChatErrorType {
  chatNotFound,
  chatNotCreated,
  userNotMember,
  chatDisabled,
}

2. Create mapper interface:

// lib/data/errors/chat_error_code_mapper.dart
abstract class ChatErrorCodeMapper {
  ChatErrorType? mapErrorCode(int serverCode);
}

3. Add to params:

// lib/declaration/chat_params.dart
class FeatureChatParams {
  final String Function() host;
  final ChatErrorCodeMapper errorCodeMapper;

  const FeatureChatParams({
    required this.host,
    required this.errorCodeMapper,
  });
}

4. Use in error handler:

// lib/data/errors/chat_error_handler_provider.dart
class ChatErrorHandlerProvider implements ErrorHandlerProvider {
  final ChatErrorCodeMapper errorCodeMapper;

  ErrorInfo? handleError(ServerApiException exception) {
    final errorType = errorCodeMapper.mapErrorCode(exception.serverCode);

    switch (errorType) {
      case ChatErrorType.chatNotFound:
        return ErrorInfo.serverCodeMessage(...);
      default:
        return null;
    }
  }
}

5. App layer provides implementation:

// app layer
class ChatErrorCodeMapperImpl implements ChatErrorCodeMapper {
  @override
  ChatErrorType? mapErrorCode(int serverCode) {
    switch (serverCode) {
      case 514: return ChatErrorType.chatNotFound;
      case 515: return ChatErrorType.chatNotCreated;
      default: return null;
    }
  }
}

ChatModule.newInstance(
  params: FeatureChatParams(
    host: () => configRepository.getApiUrlSync(),
    errorCodeMapper: ChatErrorCodeMapperImpl(),
  ),
);

Examples: feature_auth, feature_chat

Common Problems and Solutions #

Problem 1: Module Params Not Set Error #

Error:

Exception: FeatureModule params not set. Call newInstance() first.

Cause: Trying to access FeatureModule.instance before calling newInstance()

Solution:

  1. Ensure newInstance() is called before accessing .instance
  2. Initialize modules in app's init() method before calling super.init()
  3. Add modules to modules list AFTER initializing with newInstance()
// ❌ Wrong order
@override
List<BaseModule> get modules => [
  FeatureModule.instance,  // Error! Not initialized yet
];

@override
Future<void> init(AppParams params) async {
  FeatureModule.newInstance(params: ...);
  return super.init(params);
}

// ✅ Correct - initialize before accessing
@override
Future<void> init(AppParams params) async {
  FeatureModule.newInstance(params: featureParams);
  return super.init(params);
}

@override
List<BaseModule> get modules => [
  FeatureModule.instance,  // Now safe to access
];

Problem 2: Circular Dependency Between Modules #

Error:

Dependent Type X is not registered yet

Cause: Module A needs dependency from Module B, but Module B needs dependency from Module A

Solutions:

Option A: Use FactoryFunc in Params

// Instead of direct dependency in DI
// ❌ Circular dependency
class FeatureAParams {
  // Can't get from GetIt yet during params creation
}

// ✅ Use factory function
class FeatureAParams {
  final FactoryFunc<DependencyFromB> dependencyFactory;

  const FeatureAParams({
    required this.dependencyFactory,
  });
}

// In app init
FeatureAModule.newInstance(
  params: FeatureAParams(
    dependencyFactory: () => GetIt.instance.get<DependencyFromB>(),
  ),
);

Option B: Reorder Module Initialization

Ensure the module providing the dependency is initialized first:

@override
Future<void> init(AppParams params) async {
  FeatureBModule.newInstance(...);  // Provides DependencyFromB
  FeatureAModule.newInstance(...);  // Needs DependencyFromB
  return super.init(params);
}

Option C: Move Shared Dependency to Common Module

If multiple modules need the same dependency, register it in a base module:

// In base module or app-level DI
getIt.registerLazySingleton<SharedDependency>(
  () => SharedDependencyImpl(),
);

// Both modules can now access it
class FeatureAParams {
  // No need to pass - get from GetIt directly
}

Problem 3: Accessing Dependencies Before Module Setup #

Error:

GetIt: Object/factory with type X is not registered

Cause: Trying to get dependency before module's setupDependencies() is called

Solution: Use lazy evaluation in params:

// ❌ Wrong - evaluates immediately
FeatureParams(
  repository: GetIt.instance.get<MyRepo>(),  // Not registered yet!
)

// ✅ Correct - lazy evaluation
FeatureParams(
  repositoryFactory: () => GetIt.instance.get<MyRepo>(),
)

Problem 4: Hot Reload Issues with Singleton Params #

Error:

Object/factory with type FeatureParams is already registered

Cause: Hot reload tries to register params again without clearing GetIt

Solution: Check if registered before registering:

static FeatureModule newInstance({required FeatureParams params}) {
  if (!GetIt.instance.isRegistered<FeatureParams>()) {
    GetIt.instance.registerSingleton<FeatureParams>(params);
  }
  return _instance;
}

Problem 5: Params Not Available in DI Function #

Error:

GetIt: Object/factory with type FeatureParams is not registered

Cause: Trying to access params in DI setup before module registers them

Solution: Access params through GetIt in setupDI:

Future<void> setupFeatureDI(GetIt getIt, FeatureParams params) async {
  // ✅ Use params passed as argument
  getIt.registerLazySingleton<MyRepo>(
    () => MyRepoImpl(params.apiHost()),
  );

  // ❌ Don't try to get from GetIt
  // final params = getIt.get<FeatureParams>();  // Might not be registered yet
}

Problem 6: Dependency Registration Type Mismatch #

Error:

Dependent Type X is not an async Singleton

Cause: Using registerSingletonWithDependencies with wrong registration type

Solution: Match registration types or remove dependsOn:

// ❌ Wrong - mixing lazy and with dependencies
getIt.registerLazySingleton<A>(() => AImpl());
getIt.registerSingletonWithDependencies<B>(
  () => BImpl(getIt.get()),
  dependsOn: [A],  // Error: A is lazy, not singleton
);

// ✅ Option 1: Both lazy (remove dependsOn)
getIt.registerLazySingleton<A>(() => AImpl());
getIt.registerLazySingleton<B>(() => BImpl(getIt.get()));

// ✅ Option 2: Both singleton with dependencies
getIt.registerSingletonWithDependencies<A>(
  () => AImpl(),
  dependsOn: [],
);
getIt.registerSingletonWithDependencies<B>(
  () => BImpl(getIt.get()),
  dependsOn: [A],
);

Best Practices #

1. Keep Modules Focused #

  • One module per feature domain
  • Clear boundaries between modules
  • Minimal cross-module dependencies

2. Use Params for Configuration #

  • Don't access GetIt directly in params definitions
  • Use Function() for lazy evaluation
  • Use FactoryFunc<T> for dependency injection

3. Registration Types #

  • registerLazySingleton: For most repositories/services
  • registerFactory: For objects created multiple times
  • registerSingleton: For immediate initialization
  • registerSingletonWithDependencies: When explicit dependency order matters

4. Avoid Circular Dependencies #

  • Use factory functions in params
  • Consider extracting shared dependencies
  • Review module initialization order

5. Clear Resource Management #

  • Override clear() to dispose resources
  • Unsubscribe from streams
  • Close connections and timers

Examples #

See real implementations:

Basic Patterns:

  • features/feature_chat/ - Simple params (host configuration)
  • features/feature_game_impl/ - Dependency factory pattern
  • features/feature_session/ - Optional dependencies (interceptors)
  • features/feature_profile_impl/ - External service integration (purchases)

Advanced Patterns:

  • features/feature_auth/ - Complete example with:
    • Localization (LocalizationModule mixin)
    • Feature toggles (AuthFeatureName with tool_config)
    • Multiple params (API services, session provider, URLs)
    • Error mapping
    • Complex initialization

Localization:

  • features/feature_auth/lib/l10n/ - Localization structure
  • features/feature_auth/lib/declaration/auth_module.dart - LocalizationModule mixin
  • features/feature_chat/lib/l10n/ - Another localization example

Feature Toggles (tool_config):

  • features/feature_auth/lib/config/models/auth_feature_name.dart - AuthFeatureName enum implementing FeatureName
  • features/feature_auth/lib/presentation/pages/login/state/login_state.dart - FeaturesStateContract implementation
  • features/feature_auth/lib/presentation/widgets/social_panel_helper.dart - Conditional UI based on features
  • features/feature_config_api/lib/data/models/app_feature_name.dart - App-level feature names

Architecture Diagram #

┌─────────────────────────────────────────────┐
│              App Layer                      │
│  (apps/app_game/lib/game_app_modules.dart) │
│                                             │
│  1. Create Params with configuration        │
│  2. Call FeatureModule.newInstance(params)  │
│  3. Add FeatureModule.instance to modules   │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│          Feature Module Layer               │
│  (features/feature_*/declaration/)          │
│                                             │
│  • FeatureParams (config)                   │
│  • FeatureModule (singleton)                │
│  • setupFeatureDI (registration)            │
│                                             │
│  Module Lifecycle:                          │
│  1. newInstance() → registers params        │
│  2. setupDependencies() → registers DI      │
│  3. dependenciesCreated() → post-setup      │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│            GetIt Container                  │
│                                             │
│  • FeatureParams (singleton)                │
│  • Repositories (lazy singleton)            │
│  • Services (factory)                       │
│  • Use Cases (factory)                      │
└─────────────────────────────────────────────┘

Version History #

See CHANGELOG.md for version history.