FlutterのGet startedをRiverpodでやってみたのでStatefulWidgetの場合と比較してみる
FlutterのGet startedを一通りやってみたのでその備忘録です。
docs.flutter.dev
Flutterではstate管理にRiverpodが使われることが多いようなのでこちらも試してみました。
Riverpod版と通常のStatefulWidget版で比較をしつつ説明します。
Riverpodでやってみたソースはこちら github.com
説明
Riverpod版とStatefulWidget版それぞれの実装方法で異なる点について説明します。
無限スクロールの実装
Get startedの題材には無限スクロールがあります。StatefulWidget版のソースは以下の通りです。
StatefulWidget版
class _RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = <WordPair>{}; final _biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { return Scaffold( ... body: ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { if (i.isOdd) return const Divider(); final index = i ~/ 2; if (index >= _suggestions.length) { _suggestions.addAll(generateWordPairs().take(10)); } ... return ListTile( title: Text( _suggestions[index].asPascalCase, style: _biggerFont, ), ... ); }, ) ); } }
StatefulWidget版では、スクロールしてリストの要素が少なくなったら、Stateのインスタンス変数である_suggestions
にリスト要素を追加しています。
_suggestions
が無限スクロールのリストを描画するためのstateを持っています。
Riverpod版のソースは以下の通りです。
Riverpod版
class WordListNotifier extends StateNotifier<List<Word>> { ... Future<void> addAll(List<WordPair> wordPairs) async { await Future.delayed(const Duration(milliseconds: 1)); for (var wordPair in wordPairs) { state = [...state, Word(wordPair: wordPair)]; } } ... } final wordListProvider = StateNotifierProvider<WordListNotifier, List<Word>>((ref) { return WordListNotifier(); }); ... class RandomWordsState extends ConsumerState<RandomWords> { final biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { final words = ref.watch(wordListProvider); return Scaffold( ... body: ListView.separated( padding: const EdgeInsets.all(16.0), itemCount: words.length, itemBuilder: (context, index) { if (index >= words.length - 1) { ref .read(wordListProvider.notifier) .addAll(generateWordPairs().take(10).toList()); } return ListTile( title: Text( words[index].wordPair.asPascalCase, style: biggerFont, ), ... ); }, ), ); } }
Riverpod版では、StateNotifierProviderというstateを管理するクラスとStateNotifierを継承したstateクラスを使用しています。
やっていることはStatefulWidget版と同じで、リストの要素が少なくなったらリストのstateに新しいリスト要素を追加しています。
Riverpodでstateを更新する際にはimmutableに扱うことを前提としているので、以下のように既存のリストと新しいリストを組み合わせた新しいstateを作成します。
state = [...state, Word(wordPair: wordPair)];
Future.delayed
している部分はWidgetをbuild中にstateを変更しているエラーを回避するためのおまじないです。
ちなみにListView.builder
とListView.separated
はDividerがリストの要素に含まれるか、含まれないかの違いです。
個人的にわかりづらかったのでListView.separated
に変えました。
お気に入りの実装
Get startedの題材ではリストの中からお気に入りを選択する機能があります。StatefulWidget版のソースは以下の通りです。
StatefulWidget版
class _RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = <WordPair>{}; final _biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { return Scaffold( ... body: ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { ... final alreadySaved = _saved.contains(_suggestions[index]); return ListTile( ... onTap: () => { setState(() { if (alreadySaved) { _saved.remove(_suggestions[index]); } else { _saved.add(_suggestions[index]); } }) }, ); }, )); } void _pushSaved() { Navigator.of(context).push( MaterialPageRoute<void>( builder: (context) { final tiles = _saved.map( (pair) { return ListTile( title: Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); ... }, ), ); } }
_saved
のインスタンス変数にお気に入りとして選択したリストの要素を追加することで、リストの保存を実現しています。
_suggestions
の全体のリストに_saved
に保存したリストの要素があるかどうかでお気に入りに保存するか、削除するかを決めています。
_suggestions
と_saved
で変数が分かれていますが、どちらもリストのstateなので分かれていると扱いが面倒です。
Riverpod版のソースは以下の通りです。
class WordListNotifier extends StateNotifier<List<Word>> { ... void toggle(int idx) { state = [ for (int i = 0; i < state.length; i++) if (i == idx) Word(wordPair: state[i].wordPair, isFavorite: !state[i].isFavorite) else state[i], ]; } } class RandomWordsState extends ConsumerState<RandomWords> { final biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { final words = ref.watch(wordListProvider); return Scaffold( ... body: ListView.separated( padding: const EdgeInsets.all(16.0), itemCount: words.length, itemBuilder: (context, index) { ... return ListTile( ... onTap: () { ref.read(wordListProvider.notifier).toggle(index); }, ); }, ), ); } void pushSaved() { final words = ref.watch(wordListProvider); final saved = words.where((word) => word.isFavorite == true); Navigator.of(context).push( MaterialPageRoute<void>( builder: (context) { final tiles = saved.map( (pair) { return ListTile( title: Text( pair.wordPair.asPascalCase, style: biggerFont, ), ); }, ); ... }, ), ); } }
WordListNotifierのtoggleメソッドでお気に入りのstateを処理しています。
前述のとおり、immutableに扱うため、お気に入りのstateを変更した要素だけでなく、全てのリスト要素を作成しています。
全体のリストのstate内にお気に入りのstateも保持しているので、stateの扱いがわかりやすくなっています。
まとめ
Riverpodでstateを扱うためにはimmutableを意識する、Providerを利用するなど気をつけるポイントがあります。
Riverpodでのテスト方法も学習しておいた方がよいテーマだと思います。次の機会にやってみたいと思います。