[Flutter/Riverpod] Listの変更による余計なWidgetの再描画をしない方法
RiverpodでListを管理しているとき、Providerのstateに全く同じListが代入されてもref.watch
によるWidgetの再描画が発生します。
以下のようなコードだと、この現象が起きます。
final listProvider = StateProvider<List<int>>((ref) => [1, 2, 3]);
class ListPrintPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// Providerから値を読み取る
final list = ref.watch(listProvider);
return Scaffold(
appBar: AppBar(),
body: Text(list.toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 同じ値を入れても上のref.watchが反応しWidgetがリビルドされる
ref.read(listProvider.notifier).state = [1, 2, 3];
},
),
);
}
}
この余計な再描画を解消するコードを自分なりに3つほど考えたので、まとめておきます。
ref.watch
が反応するのか
そもそもなぜref.watch
は状態変化を==
演算子で確認しています。int型やString型は値を==
演算子で等価比較できますが、ListのようなIterable型は==
演算子で値を比較できません。これは値ではなく参照元を比較しているためです。
例えば、以下のlistCはlistAと同じ参照元なので比較した際はtrueになります。
List<int> listA = [1, 2, 3];
List<int> listB = [1, 2, 3];
List<int> listC = listA;
print(listA == listB); // false
print(listA == listC); // true
参照元が同じでないとtrueにならないため、値が同じかを確認するにはfoundation.dart
のlistEquals
関数か、collection.dart
のListEquality().equals
を使用します。
しかし、これらはref.watchで使用することができません。ですが次の方法であれば、同じ値が代入されても余計なWidgetの再描画が起こりません。
1. 新たにProviderを定義する
新たにProviderを用意し、そのProviderの中身に再描画の条件を書き込みます。
これで動きはしますが、書き方があまりよろしくない気がします。
final listProvider = StateProvider<List<int>>((ref) => [1, 2, 3]);
// 同じ値の代入でWidgetを再描画しないListのProvider
final comparableListProvider = Provider.autoDispose((ref) {
ref.listen<List<int>>(listProvider, (prev, next) {
// 代入される前の値と代入する値をlistEquals関数で比較し、違う場合は自身のstateを書き換える
if (!listEquals(prev, next)) {
ref.state = next;
}
});
return ref.read(listProvider);
});
class ListPrintPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 読み取りのときだけcomparableListProviderを使用する
final list = ref.watch(comparableListProvider);
return Scaffold(
appBar: AppBar(),
body: Text(list.toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(listProvider.notifier).state = [1, 2, 3];
},
),
);
}
}
2. StateNotifierでupdateShouldNotify関数を上書きする
StateNotifierを使用している場合限定ですが、updateShouldNotifyをOverrideすることでWidgetの再描画条件を書き込めます。
updateShouldNotify関数は、返す値がtrueの場合のみ再描画通知を行います。
これが一番スッキリした書き方だと個人的には思いますね。
final listProvider = StateNotifierProvider<ListNotifier, List<int>>((ref) {
return ListNotifier([1, 2, 3]);
});
class ListNotifier extends StateNotifier<List<int>> {
ListNotifier(List<int> state) : super(state);
bool updateShouldNotify(List<int> old, List<int> current) {
// ここにWidgetの再描画条件を書き込む
return !listEquals(old, current);
}
}
class ListPrintPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final list = ref.watch(listProvider);
return Scaffold(
appBar: AppBar(),
body: Text(list.toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(listProvider.notifier).state = [1, 2, 3];
},
),
);
}
}
==
演算子で比較が可能なクラスを作る
3. ==
演算子で比較が可能なIterableクラスを自分で作ります。
ここではUnmodifiableListViewクラスを継承して実装します。この実装方法が一番楽だと思いますね。
class ComparableListView<E> extends UnmodifiableListView<E> {
ComparableListView(Iterable<E> source) : super(source);
// ==演算子による処理を定義することで、値の比較が可能になる
bool operator ==(Object other) =>
identical(this, other) ||
(other is ComparableListView<E> &&
runtimeType == other.runtimeType &&
listEquals<E>(this, other));
int get hashCode => super.hashCode;
}
// Providerの定義は通常のListで行う
final listProvider = StateProvider<List<int>>((ref) => [1, 2, 3]);
class ListPrintPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// selectで比較の時だけ使用するのが良さそう
final list = ref.watch(listProvider.select((state) => ComparableListView(state)));
return Scaffold(
appBar: AppBar(),
body: Text(list.toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(listProvider.notifier).state = [1, 2, 3];
},
),
);
}
}
以上になります。
個人的には2番目の方法を最優先で用い、StateNotifierを使用しない場合は3番目の方法でやるのが一番かと思います。
Flutter勉強中の身です。何か間違いがありましたらコメントいただけると幸いです。
Discussion