flutterdartlistenerflutter-change-notifierchangenotifier

Flutter: ListenableBuilder is rebuilding the child widget


The child widget of the ListenableBuilder is rebuilt even though it claims to preserve the child widget from re-building.

In the example below counterNotifier is an instance of ChangeNotifier. I believe there is no need to see its implementation, I'm simply calling notifyListener() when changes are done.

      ListenableBuilder(
        listenable: counterNotifier,
        builder: (context, child) {
          if (counterNotifier.isLoading) {
            return const AppLoader();
          }
          if (counterNotifier.value == null) {
            return const SizedBox.shrink();
          }
          return Row(
            mainAxisAlignment: MainAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              child!,
              Text('${counterNotifier.value}'),
            ],
          );
        },
        child: const CountText(),
      ),

Solution

  • Issue

    The reason why the child is rebuilt is that I was returning the widget conditionally in the builder callback.

    This changes the widget tree as per the condition, hence the child was not preserved in the widget tree.


    Fix

    To fix this, we can use Offstage which hides the widget from view but the widget is still present in the widget tree.


    Example

    After changes the ListenableBuilder will look like this:

    return ListenableBuilder(
      listenable: listenable,
      builder: (context, child) {
        final isLoading = listenable.isLoading;
        final hasData = listenable.value != null;
        return Stack(
          children: [
            Offstage(
              offstage: !isLoading,
              child: const AppLoader(),
            ),
            Offstage( // <-- This one is unnecessary in this case. This is just for the sake of converting the code.
              offstage: !hasData,
              child: const SizedBox.shrink(),
            ),
            Offstage(
              offstage: isLoading || !hasData,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  child!,
                  Text('${listenable.value}'),
                ],
              ),
            ),
          ],
        );
      },
      child: child,
    );
    

    Note

    Instead of Stack, you can use whatever best suits you.