Skip to content

<ValueListener>

Overview

The <ValueListener> component creates a stateful widget that listens for changes to a named dependency value and rebuilds its children when that value changes. It is the primary mechanism for reactive UI updates in XWidget. Notifications are path-based: a listener rebuilds when a write path passes through the notifier created for the listener's dependency path.

Under the hood, <ValueListener> wraps the watched value in a ValueNotifier using Dependencies.listenForChanges(). When the value changes, Flutter's ValueListenableBuilder triggers a rebuild of the listener's children.

Attributes

Name Type Description Required Default
varName String The dependency key to watch for changes Yes -
initialValue dynamic Initial value to set if the dependency doesn't exist yet No null
defaultValue dynamic Default value to use if the dependency resolves to null No null
varDisposal VariableDisposal Controls how the notifier is disposed when the widget is removed. Values: none, byOwner, byLastListener No VariableDisposal.none
key Key Widget key for controlling widget identity No null
for String The name of the parent's attribute that will be assigned this component No null
visible bool Controls widget visibility No true

Variable Disposal

The varDisposal attribute controls what happens to the underlying ValueNotifier when the <ValueListener> widget is disposed:

Value Behavior
none The notifier is not disposed. The dependency value persists. This is the default.
byOwner The notifier is disposed only if this listener is the one that created it.
byLastListener The notifier is disposed only if no other listeners remain attached to it.

When a notifier is disposed, the dependency value is also removed from the Dependencies instance.

Examples

Basic Value Listening

<Controller name="CounterController">
    <ValueListener varName="count">
        <Text data="Count: ${toString(count)}"/>
    </ValueListener>
</Controller>
class CounterController extends Controller {
  var count = 0;

  @override
  void bindDependencies() {
    dependencies.setValue("count", count);
    dependencies.setValue("increment", increment);
  }

  void increment() {
    dependencies.setValue("count", ++count);
  }
}

When increment() is called, the count value is updated via setValue, which triggers the <ValueListener> to rebuild and display the new count.

Listening to Nested Model Properties

<ValueListener varName="profile">
    <Column>
        <Text data="${profile.first}"/>
        <Text data="${profile.last}"/>
        <Text data="${profile.email}"/>
    </Column>
</ValueListener>
final user = Model({
  "first": "Mike",
  "last": "Smith",
  "email": "[email protected]",
});
dependencies.setValue("profile", user);

Changes to nested properties will trigger this listener when the write goes through the listened path:

dependencies.setValue("profile.first", "Michelle");

A direct write that starts from the model object itself changes the model, but it does not pass through the profile notifier:

user.setValue("first", "Michelle");

For a deeper explanation of path-based notification, see State And Reactivity.

With Initial and Default Values

<ValueListener varName="selectedTab" initialValue="${0}" defaultValue="${0}">
    <Text data="Selected tab: ${toString(selectedTab)}"/>
</ValueListener>

initialValue sets the value if the dependency doesn't exist yet. defaultValue is used if the dependency resolves to null.

With Variable Disposal

<ValueListener varName="tempData" varDisposal="byOwner">
    <Text data="${tempData}"/>
</ValueListener>

When this listener is removed from the widget tree, the tempData dependency is also cleaned up since this listener created the notifier.

Multiple Listeners on the Same Value

Multiple <ValueListener> widgets can watch the same dependency. All of them rebuild when the value changes:

<Column>
    <ValueListener varName="count">
        <Text data="Header count: ${toString(count)}"/>
    </ValueListener>

    <!-- other widgets -->

    <ValueListener varName="count">
        <Text data="Footer count: ${toString(count)}"/>
    </ValueListener>
</Column>

See Also

  • <EventListener> - For responding to application-wide events
  • <Controller> - For managing state and exposing data via dependencies
  • <DynamicBuilder> - For async data loading