Riverpod 不必要的重建

Riverpod unnecessary rebuilds

问题是:当我单击我的一个 CharBarButtons 小部件时,所有其他 CharBarButtons 也被重建有没有办法避免这种情况并在使用 StateNotifier 持有列表时单独进行重建(整数是按钮的位置)

CharBarButtons : (这里我根据isTapped的布尔值改变CharBarButtons的装饰)


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,
                  )));
  }
}

CharBarButtons 的祖先:CharBar 小部件是一个简单的小部件,决定了应该创建多少个 CharBarButtons。

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)
                ],
              )
            ]),
    );
  }
}

这是 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 = [];
  }
}

这是提供者:

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

注意:CharBar(祖先)没有任何不必要的重建,或其他祖先。只有 CharBarButtons 进行了不必要的重建。

注意 2:我知道 contains() 方法也很昂贵,但我将列表限制为最多 12 个整数元素。所以我觉得应该问题不大。

注意 3:问题是由于这行代码(第一段代码)引起的,它在更新状态时重建所有 CharBarButtons,我无法按索引收听 manner.I 我正在听整个在这里列出。

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

注4:实际问题是,我们能否将重建限制为索引方式而不是监听整个列表状态

因为你用的是hooks,这其实是可以的。

变化:

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

至:

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

select 语句使我们的小部件仅在函数参数返回的值发生变化时才重建。

我在下面包含了一个完整的示例,供任何想自己测试的人使用。

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'),
        )
      ],
    );
  }
}

文档 here.