なになれ

IT系のことを記録していきます

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.builderListView.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でのテスト方法も学習しておいた方がよいテーマだと思います。次の機会にやってみたいと思います。