I really backed myself into a corner & have no idea how to re-design to fix this smoothly while keeping my functionality. I tried moving the reusable widget up in the tree but that was confusing and didn't work. I'm working on creating a Flutter package, in case this may incite some help. This is a question specific to my erroneous architecture/logic but also a general question as to how to create re-usable widgets with Riverpod provider so as to update ONLY necessary widget.

My problem is I have a "reusable" widget - "NumberButtonAnimation" & I have 5 of them in my "SequentialNavigator" parent in this case. They are all buttons and they are all listening to the pageIndex value in my logic services file navigatorServices. So when the page index is changed they all rebuild which is expensive and unnecessary of course.

How can I add logic or re-design so that only the moving button needs rebuild?

Would I be correct in my thinking if I thought keys could be of some use?

/// LOGIC

final navigatorServices = ChangeNotifierProvider((ref) => NavigatorServices());

class NavigatorServices extends ChangeNotifier {
  StateCRUD stateCRUD = StateCRUD();
  int pageIndex = 1;
  int indexCount = 5;

  void nextPage() {
    if (pageIndex < indexCount) {
      pageIndex++;
      saveState();
      notifyListeners();
    }
  }

  void previousPage() {
    if (pageIndex > 1) {
      pageIndex--;
      saveState();
      notifyListeners();
    }
  }

  double buttonPosition(int buttonNumber) {
    double totalHeight = Get.height;
    double topPadding = WidgetsBinding.instance!.window.padding.top;
    double bottomPadding = WidgetsBinding.instance!.window.padding.bottom;
    double safeArea = totalHeight - (topPadding + bottomPadding);
    const double buttonWithPadding = 35.0;
    late double buttonTop;
    List<int> pageIndexes = [];
    int index = 0;
    // Current or past screen
    if (pageIndex >= buttonNumber) buttonTop = ((buttonWithPadding * buttonNumber) - buttonWithPadding);
    if (pageIndex < buttonNumber) {
      while (index < indexCount + 1) {
        pageIndexes.add(index);
        index++;
      }
      buttonTop = (buttonWithPadding * pageIndexes[buttonNumber]) + safeArea / 1.5;
    }
    return buttonTop;
  }

   ...
}

/// NUMBER BUTTON ANIMATION WIDGET

class NumberButtonAnimation extends StatelessWidget {
  const NumberButtonAnimation({Key? key, required this.buttonNumber}) : super(key: key);
  final int buttonNumber;

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        print('Number button consumer built');
        return AnimatedPositioned(
          top: ref.watch(navigatorServices).buttonPosition(buttonNumber),
          curve: Curves.bounceOut,
          duration: const Duration(milliseconds: 500),
          child: NumberButton(number: buttonNumber),
        );
      },
    );
  }
}

/// PARENT WIDGET

class SequentialNavigator extends StatelessWidget {
  const SequentialNavigator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 43.0,
      height: double.maxFinite,
      child: Stack(
        fit: StackFit.expand,
        alignment: Alignment.topCenter,
        children: [
          const NumberButtonAnimation(buttonNumber: 1),
          const NumberButtonAnimation(buttonNumber: 2),
          const NumberButtonAnimation(buttonNumber: 3),
          const NumberButtonAnimation(buttonNumber: 4),
          const NumberButtonAnimation(buttonNumber: 5),
        ],
      ),
    );
  }
}

What it looks like when I tap next or previous once:

<5> flutter: Number button consumer built

This prints 5 times indicating all widgets rebuilding but only one button needs rebuilding on any change...


Solution 1: Rémi Rousselet

You can use provider.select(...) to filter rebuilds by obtaining only what you need

A usage example would be:

Consumer(
  builder: (context, ref, child) {
    print('Number button consumer built');
    return AnimatedPositioned(
      top: ref.watch(navigatorServices.select((service) => service.buttonPosition(buttonNumber)),
      ...
    );
  },
);