tool_modularity 0.0.4
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 #
- BaseModule: Abstract class that all feature modules extend
- Params Pattern: Configuration objects passed to modules during initialization
- Module Lifecycle:
setupDependencies()→dependenciesCreated()→clear() - 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 instanceinstancegetter validates params are registered- Check
isRegisteredbefore 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
_setupDIprivate (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(infeature_config_api) - Global app featuresAuthFeatureName(infeature_auth) - Module-specific features- Both implement
FeatureNamefromtool_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 #
-
Default to Safe Values: Use sensible defaults in params
const FeatureParams({ this.enableExperimentalFeature = false, // Default: off }); -
Document Feature Flags: Explain what each toggle controls
/// Enable experimental search algorithm /// WARNING: Not recommended for production final bool enableExperimentalSearch; -
Validate Combinations: Check for invalid feature combinations
FeatureParams({ required this.enableFeatureA, required this.enableFeatureB, }) { if (enableFeatureB && !enableFeatureA) { throw ArgumentError('Feature B requires Feature A'); } } -
Use Enums for Related Toggles: Group related features
enum Theme { light, dark, auto } enum LogLevel { debug, info, warning, error } -
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:
- Ensure
newInstance()is called before accessing.instance - Initialize modules in app's
init()method before callingsuper.init() - Add modules to
moduleslist AFTER initializing withnewInstance()
// ❌ 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/servicesregisterFactory: For objects created multiple timesregisterSingleton: For immediate initializationregisterSingletonWithDependencies: 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 patternfeatures/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 structurefeatures/feature_auth/lib/declaration/auth_module.dart- LocalizationModule mixinfeatures/feature_chat/lib/l10n/- Another localization example
Feature Toggles (tool_config):
features/feature_auth/lib/config/models/auth_feature_name.dart- AuthFeatureName enum implementing FeatureNamefeatures/feature_auth/lib/presentation/pages/login/state/login_state.dart- FeaturesStateContract implementationfeatures/feature_auth/lib/presentation/widgets/social_panel_helper.dart- Conditional UI based on featuresfeatures/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.