dependy_flutter 1.0.0 copy "dependy_flutter: ^1.0.0" to clipboard
dependy_flutter: ^1.0.0 copied to clipboard

Dependy for Flutter

Dependy Flutter #

Contents #

About #

Dependy Flutter is built on top of Dependy. It adds scoping features to help us manage dependencies within the widget tree of our Flutter applications.

Installation #

To add Dependy Flutter to our project, we need to update pubspec.yaml file:

dependencies:
  dependy: ^1.0.0
  dependy_flutter: ^1.0.0

Next, we run this command to install the dependencies:

flutter pub get

Import #

To use Dependy Flutter in our project, we need to import the necessary libraries:

import 'package:dependy/dependy.dart';
import 'package:dependy_flutter/dependy_flutter.dart';

Why Scoping is Important #

  • Temporary Lifespan: Some services should only last a short time. Scoping allows us to create services that we can dispose of when they’re no longer needed.

  • Specific Use Cases: Scoping works well for services tied to specific contexts, like ViewModels or state on particular screens or routes in our application. This means we can have different instances of a service for different parts of our app.

  • Resource Management: By limiting how long certain services last, we can make sure resources are freed up when they’re no longer required.

When to Use Scoping #

  • Screen-Specific Services: If we have ViewModels or services that are only relevant to a specific screen, scoping helps us manage their lifecycle without affecting other parts of our app.

  • Route-Based Scoping: When we navigate between different routes, we can create and dispose of scoped dependencies with the route. This is useful for services that shouldn’t last beyond the current route.

  • Performance Optimization: By scoping services, we can avoid unnecessary instances lingering in memory, which leads to better use of system resources.

Usage #

ScopedDependyModuleMixin #

/// In this example, we will demonstrate how to use [ScopedDependyModuleMixin].
///
/// [ScopedDependyModuleMixin] can only be applied to a [StatefulWidget].
///
/// It provides scoping functionality to the applied [Widget].

/// We apply the [ScopedDependyModuleMixin] to provide scoping.
/// This scoping manages the lifespan of the [Example1State] service.
///
/// [Example1State] will exist as long as [_MyHomePageState] does.
/// We apply the [ScopedDependyModuleMixin] to provide scoping.
/// This scoping manages the lifespan of the [Example1State] service.
///
/// [Example1State] will exist as long as [_MyHomePageState] does.
class _MyHomePageState extends State<MyHomePage> with ScopedDependyModuleMixin {
  @override
  Widget build(BuildContext context) {
    /// watchDependy is a function from [ScopedDependyModuleMixin].
    /// It accepts only a [ChangeNotifier] and triggers a rebuild each
    /// time a change is notified.
    final state = watchDependy<Example1State>();

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text("Example 1 (Using ScopedDependyModuleMixin)"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${state.counter}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          /// Here we can use the state.incrementCounter() directly.
          ///
          /// But for demonstration purposes, when we do not need to watch a service,
          /// we can use the function `dependy` from [ScopedDependyModuleMixin]
          /// to read the service without watching it.
          dependy<Example1State>().incrementCounter();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }

  /// This function must be implemented to return a module
  /// that is scoped to the lifespan of the [_MyHomePageState].
  ///
  /// Note: Do not return a singleton module here, as that module will be disposed of
  /// once the [Widget] is removed from the tree.
  /// For example:
  /// ```dart
  ///   return example1ServicesModule; // Do not do this!
  /// ```
  ///
  /// If you are not overriding or providing any extra modules or providers specifically for
  /// this [Widget], you may not need to use the [ScopedDependyModuleMixin].
  @override
  DependyModule moduleBuilder() {
    /// The module scoped to the lifespan of [_MyHomePageState].
    ///
    /// Note: [example1ServicesModule/submodules] won't dispose.
    return DependyModule(
      providers: {
        DependyProvider<Example1State>(
                  (dependy) {
            // Here we resolve the logger service from [example1ServicesModule].
            final logger = dependy<LoggerService>();

            // We finally return the instance to be used in [_MyHomePageState].
            return Example1State(logger);
          },
          dependsOn: {
            LoggerService, // Our [Example1State] depends on [LoggerService].
          },
        )
      },
      modules: {
        example1ServicesModule,
      },
    );
  }
}

ScopedDependyProvider #

/// In this example, we will demonstrate how to use [ScopedDependyProvider] widget.

/// No [ScopedDependyModuleMixin] is applied and [MyHomePage] is a [StatelessWidget]
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    /// From previous example, we learned how we can use [ScopedDependyModuleMixin]
    ///
    /// This is an alternative way how we can Scope [dependy] modules.
    ///
    /// [ScopedDependyProvider] does accept:
    /// - [builder] function which is used to build the UI and it does provide back
    /// the [BuildContext] and a [scope] object which let's us
    /// access the methods [dependy/watchDependy] as seen on the `example 1`
    ///
    /// - [moduleBuilder] functions which provides to us a [parentModule] and expects
    /// back an instance of [DependyModule] scoped to this [Widget]
    ///
    return ScopedDependyProvider(
      builder: (context, scope) {
        /// Here are are retrieving an instance of [Example2State] but also
        /// watching it for changes.
        ///
        /// Any change emitted by it will trigger a rebuild.
        final state = scope.watchDependy<Example2State>();

        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text("Example 2 (ScopedDependyProvider)"),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '${state.counter}',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              state.incrementCounter();
            },
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      },

      /// This function must be implemented to return a module
      /// that is scoped to the lifespan of the [_MyHomePageState].
      ///
      /// Note: Do not return a singleton module here, as that module will be disposed of
      /// once the [Widget] is removed from the tree.
      /// For example:
      /// ```dart
      ///   return example2ServicesModule; // Do not do this!
      /// ```
      ///
      /// If you are not overriding or providing any extra modules or providers specifically for
      /// this [Widget], you may not need to use the [ScopedDependyModuleMixin].
      moduleBuilder: (_) {
        /// The module scoped to the lifespan of [_MyHomePageState].
        ///
        /// Note: [example2ServicesModule/submodules] won't dispose.
        return DependyModule(
          providers: {
            DependyProvider<Example2State>(
                      (dependy) {
                // Here we resolve the logger service from [example2ServicesModule].
                final logger = dependy<LoggerService>();

                // We finally return the instance to be used in [_MyHomePageState].
                return Example2State(logger);
              },
              dependsOn: {
                LoggerService,
                // Our [Example2State] depends on [LoggerService].
              },
            )
          },
          modules: {
            example2ServicesModule,
          },
        );
      },
    );
  }
}


Share scope using ScopedDependyProvider #

// From the `example-1` and `example-2` we learned about [ScopedDependyModuleMixin]
// and [ScopedDependyProvider]
//
// On a more complex application, sometimes it is important to share the scope with other
// widgets to avoid props-drilling.
//
// Dependy does allow sharing scope using a [StatefulWidget] that applies [ScopedDependyModuleMixin] or
// [ScopedDependyProvider]
//
// On this example, we will use [ScopedDependyProvider] for demonstration.

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    /// Here we are providing a scope at this widget level using [ScopedDependyProvider].
    /// We are setting [shareScope] to true, which lets this scope be accessible
    /// from all descendant in the widget tree.
    return ScopedDependyProvider(
      shareScope: true,
      builder: (context, scope) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text("Example 3 (Share Scope ScopedDependyProvider)"),
          ),
          body: const Center(
            /// Notice that we are not passing any state directly to [CounterView].
            /// Instead, it will access the shared scope and retrieve the state it needs.
            child: CounterView(),
          ),

          /// Similarly, [CounterButton] will also use the shared scope to
          /// access the [CounterService] and modify its state.
          floatingActionButton: const CounterButton(),
        );
      },
      moduleBuilder: (_) {
        /// Notice that something is different from previous example.
        ///
        /// Since [CounterService] lives inside [example3ServicesModule], we are not providing it.
        ///
        /// If we need to override it, we can provide one in `providers`.
        ///
        /// Note: As we learned in previous example, we should not return [example3ServicesModule] directly
        /// from this function because when the Widget is removed from the tree, it will be disposed of. (Submodules won't be.)
        return DependyModule(
          providers: {},
          modules: {
            example3ServicesModule,
          },
        );
      },
    );
  }
}

/// [CounterButton] is responsible for incrementing the counter value.
class CounterButton extends StatelessWidget {
  const CounterButton({super.key});

  @override
  Widget build(BuildContext context) {
    /// We are using the [ScopedDependyConsumer] to isolate the rebuilds inside this widget.
    ///
    /// We can also do something similar to
    /// ```dart
    ///   final scope = getDependyScope(context);
    ///   final counterService = scope.watchDependy<CounterService>();
    /// ```
    ///
    /// But that will register the listener on the [ScopedDependyProvider] level, causing it to rebuild.
    return ScopedDependyConsumer(
      builder: (context, scope) {
        return FloatingActionButton(
          onPressed: () {
            /// When the button is pressed, we call [increment()] to update the counter.
            scope.dependy<CounterService>().increment();
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        );
      },
    );
  }
}

/// [CounterView] is responsible for displaying the current counter value.
class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    return ScopedDependyConsumer(
      builder: (context, scope) {
        /// Here we are watching [CounterService] and rebuilding the latest counter value.
        final counterService = scope.watchDependy<CounterService>();

        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counterService.counter}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        );
      },
    );
  }
}

Share scope using ScopedDependyModuleMixin #

// From the `example-3` we learned about sharing scope using [ScopedDependyProvider]
//
// On this example, we are about to use [ScopedDependyModuleMixin]

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with ScopedDependyModuleMixin {
  @override
  Widget build(BuildContext context) {
    /// While on [ScopedDependyProvider] we could set shareScope: true, using
    /// [ScopedDependyModuleMixin] we can invoke [shareDependyScope] which
    /// does achieve the exact same thing.
    return shareDependyScope(
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text("Example 4 (Share Scope ScopedDependyModuleMixin)"),
        ),
        body: const Center(
          /// Notice that we are not passing any state directly to [CounterView].
          /// Instead, it will access the shared scope and retrieve the state it needs.
          child: CounterView(),
        ),

        /// Similarly, [CounterButton] will also use the shared scope to
        /// access the [CounterService] and modify its state.
        floatingActionButton: const CounterButton(),
      ),
    );
  }

  @override
  DependyModule moduleBuilder() {
    /// Notice that something is different from previous example.
    ///
    /// Since [CounterService] lives inside [example4ServicesModule], we are not providing it.
    ///
    /// If we need to override it, we can provide one in `providers`.
    ///
    /// Note: As we learned in previous example, we should not return [example4ServicesModule] directly
    /// from this function because when the Widget is removed from the tree, it will be disposed of. (Submodules won't be.)
    return DependyModule(
      providers: {},
      modules: {
        example4ServicesModule,
      },
    );
  }
}

/// [CounterButton] is responsible for incrementing the counter value.
class CounterButton extends StatelessWidget {
  const CounterButton({super.key});

  @override
  Widget build(BuildContext context) {
    /// We are using the [ScopedDependyConsumer] to isolate the rebuilds inside this widget.
    ///
    /// We can also do something similar to
    /// ```dart
    ///   final scope = getDependyScope(context);
    ///   final counterService = scope.watchDependy<CounterService>();
    /// ```
    ///
    /// But that will register the listener on the [ScopedDependyProvider] level, causing it to rebuild.
    return ScopedDependyConsumer(
      builder: (context, scope) {
        return FloatingActionButton(
          onPressed: () {
            /// When the button is pressed, we call [increment()] to update the counter.
            scope.dependy<CounterService>().increment();
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        );
      },
    );
  }
}

/// [CounterView] is responsible for displaying the current counter value.
class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    return ScopedDependyConsumer(
      builder: (context, scope) {
        /// Here we are watching [CounterService] and rebuilding the latest counter value.
        final counterService = scope.watchDependy<CounterService>();

        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counterService.counter}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        );
      },
    );
  }
}


Share multiple scopes using ScopedDependyModuleMixin #

// From the previous example we learned about sharing scopes.
//
// On this example, we are about to demonstrate how multiple scoping works when shared using [ScopedDependyModuleMixin]
//
// App
//    -- LoggerService
//    MyHomePage
//        -- CounterService
//        CounterButton
//            -- Access LoggerService
//            -- Access CounterService
//        CounterView
//            -- Access LoggerService
//            -- Access CounterService

class _MyAppState extends State<MyApp> with ScopedDependyModuleMixin {
  @override
  Widget build(BuildContext context) {
    /// Share dependy scope with the descendants
    ///
    /// In this case, on entire App Level
    return shareDependyScope(
      child: MaterialApp(
        title:
        'Example 5 (Share Multiple Scopes using ScopedDependyModuleMixin)',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        home: const MyHomePage(),
      ),
    );
  }

  @override
  DependyModule moduleBuilder() {
    return DependyModule(
      providers: {
        // Provide the Logger service on the widget scope
        DependyProvider<LoggerService>(
                  (_) => ConsoleLoggerService(),
        ),
      },
      modules: {},
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with ScopedDependyModuleMixin {
  @override
  Widget build(BuildContext context) {
    return shareDependyScope(
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text(
                  'Example 5 (Share Multiple Scopes using ScopedDependyModuleMixin)'),
        ),
        body: const Center(
          child: CounterView(),
        ),
        floatingActionButton: const CounterButton(),
      ),
    );
  }

  @override
  DependyModule moduleBuilder() {
    return DependyModule(
      providers: {
        // Provide the [CounterService] on the widget scope
        DependyProvider<CounterService>(
                  (dependy) {
            final loggerService = dependy<LoggerService>();

            // increment step of 5
            return CounterServiceImpl(5, loggerService);
          },
          dependsOn: {
            LoggerService,
          },
        ),
      },
      modules: {
        // This function comes from [ScopedDependyModuleMixin]
        // Import it if its services are needed on this scope.
        parentModule(),
      },
    );
  }
}

/// [CounterButton] is responsible for incrementing the counter value.
class CounterButton extends StatelessWidget {
  const CounterButton({super.key});

  @override
  Widget build(BuildContext context) {
    return ScopedDependyConsumer(
      builder: (context, scope) {
        return FloatingActionButton(
          onPressed: () {
            // [LoggerService] lives two scopes on this example higher.
            scope.dependy<LoggerService>().log("CounterButton onPressed");

            /// When the button is pressed, we call [increment()] to update the counter.
            scope.dependy<CounterService>().increment();
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        );
      },
    );
  }
}

/// [CounterView] is responsible for displaying the current counter value.
class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    return ScopedDependyConsumer(
      builder: (context, scope) {
        /// Here we are watching [CounterService] and rebuilding the latest counter value.
        final counterService = scope.watchDependy<CounterService>();

        // [LoggerService] lives two scopes on this example higher.
        scope.dependy<LoggerService>().log("CounterView build");

        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counterService.counter}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        );
      },
    );
  }
}

Share multiple scopes using ScopedDependyModuleMixin #

// Same usecase as on `example-5` but using [ScopedDependyProvider]

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return ScopedDependyProvider(
      builder: (context, scope) {
        return MaterialApp(
          title:
          'Example 6 (Share Multiple Scopes using ScopedDependyProvider)',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(),
        );
      },
      moduleBuilder: (_) {
        return DependyModule(
          providers: {
            // Provide the Logger service on the widget scope
            DependyProvider<LoggerService>(
                      (_) => ConsoleLoggerService(),
            ),
          },
          modules: {},
        );
      },
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ScopedDependyProvider(
      builder: (context, scope) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text(
              'Example 6 (Share Multiple Scopes using ScopedDependyProvider)',
            ),
          ),
          body: const Center(
            child: CounterView(),
          ),
          floatingActionButton: const CounterButton(),
        );
      },
      moduleBuilder: (parentModule) {
        return DependyModule(
          providers: {
            // Provide the [CounterService] on the widget scope
            DependyProvider<CounterService>(
                      (dependy) {
                final loggerService = dependy<LoggerService>();

                // increment step of 5
                return CounterServiceImpl(5, loggerService);
              },
              dependsOn: {
                LoggerService,
              },
            ),
          },
          modules: {
            // We are importing the parent module
            parentModule(),
          },
        );
      },
    );
  }
}

/// [CounterButton] is responsible for incrementing the counter value.
class CounterButton extends StatelessWidget {
  const CounterButton({super.key});

  @override
  Widget build(BuildContext context) {
    return ScopedDependyConsumer(
      builder: (context, scope) {
        return FloatingActionButton(
          onPressed: () {
            // [LoggerService] lives two scopes on this example higher.
            scope.dependy<LoggerService>().log("CounterButton onPressed");

            /// When the button is pressed, we call [increment()] to update the counter.
            scope.dependy<CounterService>().increment();
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        );
      },
    );
  }
}

/// [CounterView] is responsible for displaying the current counter value.
class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    return ScopedDependyConsumer(
      builder: (context, scope) {
        /// Here we are watching [CounterService] and rebuilding the latest counter value.
        final counterService = scope.watchDependy<CounterService>();

        // [LoggerService] lives two scopes on this example higher.
        scope.dependy<LoggerService>().log("CounterView build");

        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counterService.counter}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        );
      },
    );
  }
}

2
likes
140
pub points
0%
popularity

Publisher

unverified uploader

Dependy for Flutter

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

dependy, flutter

More

Packages that depend on dependy_flutter