Dependents

pub version live demo example

dependents is a Flutter package that simplifies building reactive UIs by providing widgets for managing and responding to changes in dependencies, such as inherited widgets, ValueNotifier, or any Listenable. It helps you avoid boilerplate, reduce coupling, and keep your widget tree clean and maintainable.

Why use Dependents?

final messageNotifier = ValueNotifier<String>('Hello!');

DependentBuilder<(String, bool)>(
  listenable: messageNotifier,
  dependency: (context) {
    final enclose = MediaQuery.maybeViewInsetsOf(context).bottom > 0
    return (
      enclose ? "[ ${messageNotifier.value} ]" : messageNotifier.value, 
      MediaQuery.sizeOf(context).width < 300
    );
  },
  builder: (context, _) {
    final (message, isSmall) = DependentBuilder.dependencyOf<(String, bool)>(context);
    return Text(message, softWrap: isSmall);
  },
)

This example demonstrates how DependentBuilder efficiently manages multiple dependencies.

The dependency function is triggered whenever any of the following change:

  • messageNotifier.value
  • MediaQuery.viewInsets
  • MediaQuery.size

The builder rebuilds the Text widget only when the dependency tuple (message, isSmall) changes:

  • the message value,
  • the zero/non-zero state of MediaQuery.viewInsets.bottom,
  • or when the screen width crosses the 300-pixel threshold.

By tracking several dependencies together, you ensure that your UI updates only when truly relevant changes occur, minimizing unnecessary rebuilds and improving performance.

This approach minimizes unnecessary rebuilds and improves performance compared to traditional listeners.

Flutter's widget tree is powerful, but handling dynamic dependencies and rebuilding widgets efficiently can be challenging. Typical problems include:

  • Manual state management: Tracking dependencies and updating widgets manually leads to verbose and error-prone code.
  • InheritedWidget limitations: Accessing and reacting to inherited values often requires custom logic and can be hard to scale.
  • Listenable/ValueNotifier boilerplate: Listening to changes and rebuilding widgets usually involves repetitive code.
  • Unintended rebuilds: Widgets may rebuild unnecessarily, impacting performance.

dependents solves these problems by:

  • Automatically rebuilding widgets only when relevant dependencies change.
  • Providing a unified API for both inherited widgets and listenable objects.
  • Allowing you to react to dependency changes with custom callbacks (side effects).
  • Making dependency access and propagation simple and type-safe.

Features

  • Automatic rebuilds: Widgets update only when their dependencies change.
  • Unified dependency management: Works with inherited widgets, ValueNotifier, and any Listenable.
  • Custom listeners: React to changes with side effects, not just rebuilds.
  • Type-safe dependency access: Easily get the current value anywhere in the widget subtree.
  • Composable and lightweight: Integrates with other state management solutions and fits any architecture.

Getting started

Add the following to your pubspec.yaml:

dependencies:
  dependents: <latest_version>

Import the package:

import 'package:dependents/dependents.dart';

Usage

Rebuild on InheritedWidget Special Property Change

DependentBuilder<Brightness>(
  dependency: (context) => Theme.of(context).colorScheme.brightness,
  builder: (context, _) {
    final brightness = DependentBuilder.dependencyOf<Brightness>(context);
    return Text("$brightness"),
)

In this example, the builder will only rebuild when the specific property colorScheme.brightness of the theme changes, not on every theme update or unrelated widget change.

Note: The value returned from the dependency handler can be accessed inside the builder using DependentBuilder.dependencyOf<T>(context). This allows you to retrieve the current dependency value anywhere within the builder, ensuring type safety and convenience.

Rebuild on Listenable Special Property Change

final userValueNotifier = ValueNotifier<User>(User(name: 'Dep', age: 22));

DependentBuilder<String>(
  listenable: userValueNotifier,
  dependency: (context) => userValueNotifier.value.name,
  builder: (context, _) => Text(userValueNotifier.value.name),
)

You can track changes to a specific property (e.g., name) rather than the whole object.

The builder will only rebuild when userValueNotifier.value.name changes, not when other properties change.

Rebuild on Expression depending on InheritedWidget

DependentBuilder<bool>(
  dependency: (context) => Theme.of(context).colorScheme.onPrimary.computeLuminance() > 0.5,
  builder: (context, _) {
    final light = DependentBuilder.dependencyOf<bool>(context);
    return Text(light ? "LIGHT" : "DARK");
  },
)

The dependency function can return any computed value, not just objects or direct properties from dependencies. This allows you to derive and react to complex conditions or calculations based on multiple sources, making your widgets more flexible and expressive.

Combined Listenable and Inherited Dependencies

final userNameNotifier = ValueNotifier<String>('Dep');

DependentBuilder<(bool, String)>(
  listenable: userNameNotifier,
  dependency: (context) {
    final isLight = Theme.of(context).colorScheme.brightness == Brightness.light;
    final name = userNameNotifier.value;
    return (isLight, name);
  },
  builder: (context, _) {
    final (isLight, name) = DependentBuilder.dependencyOf<(bool, String)>(context);
    return Text('Good ${isLight ? 'morning' :'evening'}, $name!');
  },
)

This widget will rebuild and update the greeting whenever the theme brightness or the user's name changes, demonstrating how to combine inherited and listenable dependencies.

Tip: If you need to track multiple listenable dependencies (such as several ValueNotifiers), you can use the listenableList property:

DependentBuilder(
  listenableList: [notifier1, notifier2],
  dependency: (context) => computeValue(notifier1.value, notifier2.value),
  builder: (context, _) {
    final computedValue = DependentBuilder.dependencyOf(context);
    return Text("$computedValue"),
  }
)

React to Dependency Changes with Side Effects

DependencyListener<String>(
  dependency: (context) => Theme.of(context).colorScheme.brightness == Brightness.light;,
  listener: (light) => print('Good ${light ? 'morning' :'evening'}!'),
  child: Container(),
)

With DependencyListener, you can react to dependency changes and trigger side effects (such as logging, analytics, or navigation) without rebuilding the widget itself. This is useful for handling events or actions that should not affect the UI layout.

listenable and listenableList are also available for handling complex dependencies.

Typical use cases

  • Theme and localization: Automatically update widgets when theme or locale changes.
  • User/session data: React to changes in user profile, authentication state, or settings.
  • Custom inherited models: Simplify access and updates for custom inherited widgets.
  • Form and input state: Rebuild or react to changes in form fields or validation state.

Strengths

  • Minimal boilerplate: Write less code to achieve robust reactivity.
  • Performance: Only affected widgets rebuild, reducing unnecessary work.
  • Flexibility: Works with any Listenable, ValueNotifier, or inherited widget.
  • Side effects: Easily trigger actions (logging, analytics, etc.) on dependency changes.
  • Type safety: Dependency values are strongly typed and easy to access.

Additional information

See the /example folder to explore practical usage patterns and integration strategies.

Issues and suggestions are welcome!

li

Libraries

dependents
This package provides Flutter widgets for building and reacting to changes in dependencies, such as inherited widgets or listenable objects, making it easier to manage and respond to dynamic data in the widget tree.