Lifecycle Provider

The LifecycleProvider is built upon the LifecycleOwner, a mixin that provides fine-grained control over a widget’s lifecycle.
While LifecycleOwner focuses on managing lifecycle events, dependency scopes, and observing live data, LifecycleProvider encapsulates this functionality into a reusable widget.
This encapsulation makes it easier to integrate lifecycle management and dependency injection directly into the UI, providing a cleaner and more maintainable structure for Flutter applications.

Key Features:

  1. Encapsulates the functionality of LifecycleOwner for easier reuse.
  2. Automatically manages dependency scope registration and cleanup.
  3. Simplifies the lifecycle callbacks with onInit, onReady, and onDispose.
  4. Integrates seamlessly with ViewModel and live data observers.
  5. Reduces boilerplate code, allowing developers to focus on business logic.

How It Works:

The LifecycleOwner mixin provides the foundation for managing a widget’s lifecycle. It handles:
  • Initializing and disposing of dependencies via DiScope.
  • Observing changes in live data through ObserverMixin.
  • Registering lifecycle events (onInitState, onReady, and onDispose)

LifecycleProvider

The LifecycleProvider encapsulates the LifecycleOwner logic into a StatefulWidget, allowing developers to manage lifecycles declaratively.
Instead of directly using LifecycleOwner, the LifecycleProvider enables you to define your dependencies, lifecycle events, and UI in a single place.
/// manage widget life-cycle with your state-holder (viewModel) and observing data,
/// and drop passed scope when widget destroyed.
class LifecycleProvider<T extends BaseViewModel> extends StatefulWidget {
  /// [DiScope] that [LifecycleOwner] will take care, register it on [initState] and drop it on [dispose]
  final DiScope diScope;

  /// [builder] is Your UI Tree after registering [diScope]
  /// use your [T] registered [instance] inside the tree
  final Widget Function(BuildContext context, T instance) builder;

  /// [onInit] is callback fired immediately after the widget is allocated in memory.
  final Function(BuildContext context, T instance)? onInit;

  /// [onReady] is callback fired 1 frame after onInit(). It is the perfect place to enter
  /// navigation, events, like snackBar, dialogs, or a new route
  final Function(BuildContext context, T instance)? onReady;

  /// [onDispose] is callback fired when widget destroyed.
  final Function(BuildContext context, T instance)? onDispose;

  const LifecycleProvider({
    required this.diScope,
    required this.builder,
    this.onInit,
    this.onReady,
    this.onDispose,
    super.key,
  });
}

Implement LifecycleProvider:

  • login_screen.dart
  • login_viewmodel.dart
class LoginScreen extends StatelessWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return LifecycleProvider<LoginViewModel>(
      diScope: DiScope(
        name: 'login', // same of @LazySingleton scope name
        factory: () => GetIt.instance.initLoginScope(), // 'initLoginScope()' generated by injectable generator
        dependencies: [
            // add 'login' scope required dependencies (list of DiScope) if exist, such as 'authentication' repository or data-source scopes
        ],
      ), 
      onInit: (context, viewModel) {
        print('widget initialized');
      };
      onReady: (context, viewModel) {
        print('widget 1st frame rendered');
      };
      onDispose: (context, viewModel) {
        print('widget disposed');
      };
      builder: (BuildContext context, LoginViewModel viewModel) {
        return Container(); /* UI Tree */
      },
    );
  }
}

When to Use LifecycleProvider vs LifecycleOwner:

The choice between LifecycleProvider and LifecycleOwner depends on the complexity of your widget’s logic and lifecycle requirements.

LifecycleOwner

Use LifecycleOwner when you need direct control over lifecycle events, particularly in scenarios where you need to:
  • Handle complex logic: Managing navigation, listening for real-time events, or handling data changes.
  • React to live data updates: Managing navigation, listening for real-time events, or handling data changes.
  • Customize lifecycle behavior: Directly observe and handle changes in live data variables.
The LifecycleOwner mixin is ideal for widgets where lifecycle handling is tightly coupled with the business logic, making your code cleaner and more readable.

Example Use Case:

  • A widget that displays real-time updates (e.g., a live sports score app).
  • Navigating to another screen or showing dialogs based on events.
  • Widgets with highly interactive behaviors like animations triggered by data updates.
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> with LifecycleOwner<LoginScreen, LoginViewModel>, ObserverMixin {
  @override
  void observeChanges(ObserverMixin observer) {
    viewModel.loginResponse.observe(observer, (LoginResponse response) {
      // navigate to home when login success
      if (response != null) _navigateHome();
    }
  }

  // navigate to home screen
  void _navigateHome() {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (_) => HomeScreen()),
    );
  }
}

LifecycleProvider

Use LifecycleProvider when your widget has simple lifecycle requirements or you want to encapsulate lifecycle handling for cleaner integration with the UI.
  • No complex logic: When your widget doesn’t require navigation, listening to events, or managing intricate data flows.
  • Focus on UI building: When the main goal is rendering UI based on a ViewModel without extensive lifecycle customization.
  • Quick integration: When you want to manage dependencies and live data without manually implementing lifecycle callbacks.
The LifecycleProvider simplifies lifecycle handling, making it ideal for widgets that focus solely on UI rendering with minimal logic.

Example Use Case:

  • A widget that displays static or moderately dynamic data.
  • Simple state management based on a ViewModel.
  • Reducing boilerplate in standard widget implementations.
class ProductScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LifecycleProvider<ProductViewModel>(
      diScope: DiScope(
        name: 'products',
        factory: getIt.initProductsScope,
      ),
      builder: (BuildContext context, ProductViewModel viewModel) {
        return LiveDataBuilder<bool>(
          data: viewModel.loading,
          builder: (BuildContext context, bool loading) {
            // display loading
            if (loading) return const CircularProgressIndicator.adaptive();
            
            // display product info
            return LiveDataBuilder<Product>(
              data: viewModel.product,
              builder: (context, product) => Column(
                children: [
                  Image.network(product.image),
                  Text(product.title),
                ],
              ),
            );
          },
        );
      },
    );
  }
}

Guidelines for Choosing

Scenario
Use
Need direct control over lifecycle callbacksLifecycleOwner
Require complex navigation or event handlingLifecycleOwner
Observing and reacting to live data changesLifecycleOwner
Only rendering UI with a ViewModel instanceLifecycleProvider
Minimal or no custom lifecycle logic requiredLifecycleProvider
Quick setup for dependency management in a widgetLifecycleProvider
By using LifecycleOwner for logic-intensive widgets and LifecycleProvider for UI-focused widgets, you can maintain a clean, readable, and scalable architecture in your Flutter applications.