Lifecycle Awareness
In Flutter applications, managing dependencies efficiently across different screens and flows can become complex, especially when dealing with multiple dependencies that have varying lifecycles.To simplify this, XFlutter-cli provides the LifecycleOwner mixin, which automates the management of dependency injection scopes, particularly when working with the injectable package.
This ensures that dependencies are properly registered and disposed of in sync with the screen lifecycle.
Role of LifecycleOwner
The primary role of LifecycleOwner is to manage the lifecycle of the dependencies like (State-Holder, Repository). These dependencies are typically marked with @LazySingleton(scope: 'scopeName'), ensuring that it is scoped to a specific feature or flow in your application.With LifecycleOwner, you control the dependencies lifecycle by automatically creating an instance when the screen initializes (initState) and cleaning it from memory when the screen is disposed (dispose).This approach optimizes resource usage and ensures that only the necessary objects are kept alive as long as they are needed.The integration with injectable allows you to define the scope of these singletons, ensuring they are automatically cleaned up when no longer required, preventing memory leaks and unnecessary resource usage.Here’s a typical usage example in a login flow:
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with LifecycleOwner<LoginScreen, LoginViewModel>, ObserverMixin {
@override
DiScope get diScope => DiScope(
name: 'login',
factory: GetIt.instance.initLoginScope,
dependencies: [
DiScope(
name: 'authentication',
factory: GetIt.instance.initAuthenticationScope,
),
],
);
@override
Widget build(BuildContext context) {
// Build the UI tree
return Container(); // Your actual UI here
}
}
Understanding DiScope
The DiScope class is at the heart of this lifecycle-aware approach. It allows you to define a scope, register its dependencies, and automatically dispose of it when it’s no longer needed. Let’s break down what each part of DiScope does:@freezed
class DiScope with _$DiScope {
const factory DiScope({
/// Name of scope.
required String name,
/// Factory method to register dependencies for the scope.
required GetIt Function() factory,
/// drop scope on dispose.
@Default(false) bool dispose, // Allows manual disposal
/// Automatically drop the scope on lifecycle-owner dispose (true by default).
@Default(true) bool disposeByOwner,
/// Other scopes or dependencies that need to be registered first.
@Default([]) List<DiScope> dependencies,
}) = _DiScope;
}
- name: This is the identifier for the scope, which helps in managing and referencing the scope.
- factory: This is a function that initializes the scope’s dependencies. In the example above, getIt.initLoginScope() is the factory method that registers the dependencies for the login scope.
- dispose: Controls whether the scope can be disposed independently of its LifecycleOwner.
If dispose is set to true, the scope can be manually disposed outside the context of the screen lifecycle.
This is useful in cases where you may want to explicitly clean up resources before the screen’s lifecycle ends. - disposeByOwner: This determines whether the scope is disposed when the lifecycle owner (e.g., the screen) is disposed.
- dependencies: This is an array of other scopes that need to be initialized before the current scope. For example, the LoginViewModel might depend on an AuthRepository which is provided by the authentication scope.
Adding Dependencies in diScope:
The dependencies field in DiScope is particularly useful when a screen’s ViewModel relies on other objects, such as repositories or pagination controllers. By specifying these dependencies in the diScope, you ensure they are registered and available to the ViewModel when the screen initializes.@override
DiScope get diScope => DiScope(
name: 'login',
factory: GetIt.instance.initLoginScope,
dependencies: [
DiScope(
name: 'authentication',
factory: GetIt.instance.initAuthenticationScope,
),
],
);
Benefits of Scoped Dependencies
- Efficient Memory Usage: By using scoped singletons, you ensure that large or complex objects (like repositories and view models) are only created when they are needed and cleaned up when they are no longer required. This prevents memory leaks and keeps the app responsive.
- Separation of Concerns: The dependencies of a particular feature, like login or authentication, are isolated within their own scope. This ensures that different parts of the app don’t interfere with each other and can be developed independently.
- Reusability: The same ViewModel, Repository, or other business logic classes can be reused across multiple components (like login, phone verification, etc.) without duplicating instances or data.
- Optimized Performance: By automatically cleaning up unused scopes and dependencies, the app minimizes unnecessary resource consumption, which helps maintain high performance even in complex flows.