Riverpod unnecessary rebuilds

Viewed 690

Problem is : When i click one of my CharBarButtons widget, all the other CharBarButtons are also rebuilt is there a way to avoid that and do the rebuilds individually while using StateNotifier holding List(integers are positions of buttons)

CharBarButtons : (Here i am changing the decoration of CharBarButtons according to the isTapped's boolean value)


class CharBarButtons extends HookWidget {
  CharBarButtons({required this.index});
  final int index;
  @override
  Widget build(BuildContext context) {
    final isTapped = useProvider(IndexStackProvider).contains(index);
    final _randomBar = useProvider(randomBarProvider);
    final usableWidth = MediaQuery.of(context).size.width -
        (MediaQuery.of(context).padding.left +
            MediaQuery.of(context).padding.right);
    final usableHeight = MediaQuery.of(context).size.height -
        (MediaQuery.of(context).padding.bottom +
            MediaQuery.of(context).padding.top);
    return InkWell(
        onTap: () {
          if (isTapped == false) {
            context.read(IndexStackProvider.notifier).append(index);
          }
        },
        child: AnimatedContainer(
            curve: Curves.easeOutExpo,
            duration: Duration(milliseconds: 125),
            child: Center(
                child: Center(
                    child: Opacity(
              opacity: isTapped == false ? 1 : 0.5,
              child: Text('${_randomBar[index]}',
                  style: TextStyle(
                      color: Colors.black87,
                      fontWeight: FontWeight.bold,
                      fontSize: usableHeight * 0.03)),
            ))),
            width: usableWidth * 0.12,
            height: usableWidth * 0.12,
            decoration: isTapped == false
                ? BoxDecoration(
                    color: const Color.fromRGBO(255, 227, 212, 1),
                    border: Border.all(
                        color: Colors.black.withOpacity(0.6), width: 2),
                    borderRadius: BorderRadius.circular(usableWidth * 0.02),
                    shape: BoxShape.rectangle,
                  )
                : BoxDecoration(
                    color: Color.fromRGBO(199, 177, 165, 1),
                    border: Border.all(
                        color: Colors.black.withOpacity(0.3), width: 2),
                    borderRadius: BorderRadius.circular(usableWidth * 0.02),
                    shape: BoxShape.rectangle,
                  )));
  }
}

Ancestor of CharBarButtons : CharBar widget is a simple widget , decides how many CharBarButtons should be created.

class CharBar extends HookWidget {
  const CharBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final usableWidth = MediaQuery.of(context).size.width -
        (MediaQuery.of(context).padding.left +
            MediaQuery.of(context).padding.right);
    final usableHeight = MediaQuery.of(context).size.height -
        (MediaQuery.of(context).padding.bottom +
            MediaQuery.of(context).padding.top);
    final _randomChars = useProvider(randomBarProvider);
    return Container(
      height: usableWidth * 0.24,
      width: usableWidth,
      child: _randomChars.length <= 6
          ? Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
              for (var i = 0; i < _randomChars.length; i++)
                CharBarButtons(index: i)
            ])
          : Column(children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  for (var i = 0; i < _randomChars.length ~/ 2; i++)
                    CharBarButtons(index: i)
                ],
              ),
              SizedBox(height: usableHeight * 0.05),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  for (var i = _randomChars.length ~/ 2;
                      i < _randomChars.length;
                      i++)
                    CharBarButtons(index: i)
                ],
              )
            ]),
    );
  }
}

Here is the StateNotifier :

class IndexStackState extends StateNotifier<List<int>> {
  IndexStackState() : super([]);
  void append(int index) {
    state = [...state, index];
  }

  void delete() {
    state = [...state.sublist(0, state.length - 1)];
  }

  void reset() {
    state = [];
  }
}

Here is the provider :

final IndexStackProvider =
    StateNotifierProvider.autoDispose<IndexStackState, List<int>>(
        (ref) => IndexStackState());

Note : CharBar (ancestor) doesn't have any unnecesary rebuilds , or another ancestor. Only the CharBarButtons are having unnecessary rebuilds.

Note 2 : I know that contains() method is also expensive but i limited the list to max 12 integer elements . So that i think it shouldn't be that much problem.

Note 3 : Problem is causing because of this line of code(first segment of code) and it rebuilds all the CharBarButtons when state is updated, i cannot listen in a index-wise manner.I am listening whole List here.

final isTapped = useProvider(IndexStackProvider).contains(index);

Note 4 : Actual problem is that, can we limit the rebuilds into a index-wise manner rather than listening whole list state

1 Answers

Because you are using hooks, this is actually possible.

Change:

final isTapped = useProvider(IndexStackProvider).contains(index);

to:

final isTapped = useProvider(indexStackProvider.select((value) => value.contains(index)));

The select statement causes our widgets to only rebuild when the value returned by the function parameter changes.

I've included a complete example below for anyone who wants to test it out themselves.

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class IndexStackState extends StateNotifier<List<int>> {
  IndexStackState() : super([]);

  void append(int index) {
    state = [...state, index];
  }

  void delete() {
    state = [...state.sublist(0, state.length - 1)];
  }

  void reset() {
    state = [];
  }
}

final indexStackProvider =
    StateNotifierProvider.autoDispose<IndexStackState, List<int>>((ref) => IndexStackState());

class ExampleWidget extends HookWidget {
  const ExampleWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: List.generate(5, (index) => TestWidget(index)),
        ),
      ),
    );
  }
}

class TestWidget extends HookWidget {
  const TestWidget(this.index, {Key? key}) : super(key: key);

  final int index;

  @override
  Widget build(BuildContext context) {
    final isTapped = useProvider(indexStackProvider.select((value) => value.contains(index)));
    debugPrint('Building index: $index');

    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Center(
          child: Text('$index: ${isTapped.toString()}'),
        ),
        SizedBox(width: 16),
        ElevatedButton(
          onPressed: () {
            final notifier = context.read(indexStackProvider.notifier);
            isTapped ? notifier.delete() : notifier.append(index);
          },
          child: Text(isTapped ? 'Delete' : 'Append'),
        )
      ],
    );
  }
}

Docs here.

Related