【Flutter】checkboxの状態管理をいろんなパターンで試す

2023/01/14に公開

チェックボックスの状態管理をいろんなパターンで試してみた。
正解とか不正解とかではなく、こういった方法でも実装できるよねって感じで。

【Stateful】

・シンプルなCheckbox

サンプル通りのシンプルなチェックボックスをStatefulで。

コード
class CheckboxSingle extends StatefulWidget {
  const CheckboxSingle({Key? key}) : super(key: key);

  
  State<CheckboxSingle> createState() => _CheckboxSingleState();
}

class _CheckboxSingleState extends State<CheckboxSingle> {
  bool _isChecked = false;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("checkbox sample")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_isChecked ? "ON" : "OFF"),
            Checkbox(
              value: _isChecked,
              onChanged: (value) {
                setState(() {
                  _isChecked = !_isChecked;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

・Checkboxを複数表示(List)

ListViewで複数のチェックボックスを表示。
値はListで管理。

コード
class CheckboxListTiles extends StatefulWidget {
  const CheckboxListTiles({Key? key}) : super(key: key);

  
  State<CheckboxListTiles> createState() => _CheckboxListTilesState();
}

class _CheckboxListTilesState extends State<CheckboxListTiles> {
  final List<String> _valueList = ['A', 'B', 'C', 'D'];
  final List<bool> _checkedList = List.generate(4, (index) => false);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("CheckboxListTile List Ver")),
      body: Center(
        child: ListView.separated(
          itemBuilder: (context, index) => CheckboxListTile(
            title: Text(_valueList[index]),
            subtitle: Text(_checkedList[index] ? "ON" : "OFF"),
            value: _checkedList[index],
            onChanged: (bool? checkedValue) {
              setState(() {
                _checkedList[index] = checkedValue!;
              });
            },
          ),
          separatorBuilder: (context, index) {
            return const Divider(height: 0.5);
          },
          itemCount: _valueList.length,
        ),
      ),
    );
  }
}

・Checkboxを複数表示(Map)

.map()で複数のチェックボックスを表示。
値はList<Map>で管理。(動作はListパターンと一緒)

コード
class CheckboxListTilesVer2 extends StatefulWidget {
  const CheckboxListTilesVer2({Key? key}) : super(key: key);

  
  State<CheckboxListTilesVer2> createState() => _CheckboxListTilesVer2();
}

class _CheckboxListTilesVer2 extends State<CheckboxListTilesVer2> {
  final List<Map<String, dynamic>> _checkedMaps = [
    {'value': 'A', 'checked': false},
    {'value': 'B', 'checked': false},
    {'value': 'C', 'checked': false},
    {'value': 'D', 'checked': false},
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("CheckboxListTile Map Ver")),
      body: Center(
        child: Column(
          children: _checkedMaps
              .map((e) => CheckboxListTile(
                    title: Text(e['value']),
                    subtitle: Text(e['checked'] ? "ON" : "OFF"),
                    value: e['checked'],
                    onChanged: (bool? checkedValue) {
                      setState(() {
                        e['checked'] = checkedValue;
                      });
                    },
                  ))
              .toList(),
        ),
      ),
    );
  }
}


【Riverpod】

※挙動は全てStatefulの時と同じのためgifは割愛

・シンプルなCheckbox

サンプル通りのシンプルなチェックボックスをRiverpod:StateProviderで。

コード
class CheckboxRiverpod extends ConsumerWidget {
  CheckboxRiverpod({Key? key}) : super(key: key);

  final AutoDisposeStateProvider<bool> _isCheckedProvider =
      StateProvider.autoDispose((ref) {
    return false;
  });

  
  Widget build(BuildContext context, WidgetRef ref) {
    final bool isChecked = ref.watch(_isCheckedProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("Checkbox + Riverpod")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(isChecked ? "ON" : "OFF"),
            Checkbox(
              value: isChecked,
              onChanged: (bool? checkedValue) {
                ref.read(_isCheckedProvider.notifier).state = checkedValue!;
              },
            ),
          ],
        ),
      ),
    );
  }
}

・Checkboxを複数表示(List)

ListViewで複数のチェックボックスを表示。
値はListで管理。

コード
class CheckboxListTileRiverpod extends ConsumerWidget {
  CheckboxListTileRiverpod({Key? key}) : super(key: key);
  final List<String> _valueList = ['A', 'B', 'C', 'D'];
  final AutoDisposeStateProvider<List<bool>> _checkedListProvider =
      StateProvider.autoDispose<List<bool>>((ref) {
    return List.generate(4, (index) => false);
  });

  final ProviderListenable<dynamic> _changeNotifierProvider =
      ChangeNotifierProvider((ref) {
    return ChangeNotifier();
  });

  
  Widget build(BuildContext context, WidgetRef ref) {
    final List<bool> checkedList = ref.watch(_checkedListProvider);
    final changeNotifier = ref.watch(_changeNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("Checkbox + Riverpod List Ver")),
      body: Center(
        child: ListView.separated(
          itemBuilder: (context, index) => CheckboxListTile(
            title: Text(_valueList[index]),
            subtitle: Text(checkedList[index] ? "ON" : "OFF"),
            value: checkedList[index],
            onChanged: (bool? checkedValue) {
              ref.read(_checkedListProvider.notifier).state[index] =
                  checkedValue!;
              changeNotifier.notifyListeners();
            },
          ),
          separatorBuilder: (context, index) {
            return const Divider(height: 0.5);
          },
          itemCount: _valueList.length,
        ),
      ),
    );
  }

・Checkboxを複数表示(Map)

.map()で複数のチェックボックスを表示。
値はList<Map>で管理。(動作はListパターンと一緒)
※Riverpodの使用方法として良くない気はするが、一応機能はしてる。。

コード
class CheckboxListTileRiverpodVer2 extends ConsumerWidget {
  CheckboxListTileRiverpodVer2({Key? key}) : super(key: key);
  final AutoDisposeStateProvider<List<Map<String, dynamic>>>
      _checkedMapStateProvider =
      StateProvider.autoDispose<List<Map<String, dynamic>>>((ref) {
    return [
      {'value': 'A', 'checked': false},
      {'value': 'B', 'checked': false},
      {'value': 'C', 'checked': false},
      {'value': 'D', 'checked': false},
    ];
  });

  final ProviderListenable<dynamic> _changeNotifierProvider =
      ChangeNotifierProvider((ref) {
    return ChangeNotifier();
  });

  
  Widget build(BuildContext context, WidgetRef ref) {
    final List<Map<String, dynamic>> checkedMap =
        ref.watch(_checkedMapStateProvider);
    final changeNotifier = ref.watch(_changeNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("Checkbox + Riverpod Map Ver")),
      body: Center(
        child: Column(
          children: checkedMap
              .map((e) => CheckboxListTile(
                    title: Text(e['value']),
                    subtitle: Text(e['checked'] ? "ON" : "OFF"),
                    value: e['checked'],
                    onChanged: (bool? checkedValue) {
                      e['checked'] = checkedValue;
                      changeNotifier.notifyListeners();
                    },
                  ))
              .toList(),
        ),
      ),
    );
  }
}

コメント

  • 状態を格納する変数をListにするかMapにするかは、実際使用するデータ構造によって使い分けか。
    今後他にもパターンがあれば追記していく。
  • Riverpodの複数パターンの時、値の更新をnotifyListeners()で行っているのが個人的にはもやもや、、
    StateProviderだけで完結させたかったが、Listの要素の値変更は上手く調整しないと監視されないみたい。
    ここをもうちょっと調べていい感じにしてみる。

追記

Riverpodでの複数パターンの時、そもそもStateProviderの使用が良くなかった。。
ListやMapを扱う場合は、以下の公式のドキュメントの文言より、StateNotifireProviderを使用する。

StateProvider は次のようなステートを公開するために使うべきではありません。
・ステート自体が複雑なオブジェクトである(カスタムのクラスや List/Map など)

・【修正版】Checkboxを複数表示(List)

ListViewで複数のチェックボックスを表示。
値は、ListをStateNotifireProviderで管理。
Listの中身が更新※されたら、画面が再生成される。

※正しくは、「更新」ではなく、「List自体を上書き」が表現としては正しい。

→詳細記事:【Flutter】【Riverpod 1.0系】StateProviderとStateNotifierProviderの違い

コード
final checkedListProvider =
    StateNotifierProvider.autoDispose<CheckedListState, List<bool>>(
  (ref) => CheckedListState(),
);

class CheckedListState extends StateNotifier<List<bool>> {
  CheckedListState() : super(List.generate(4, (index) => false));

  void updateListElement(int index, bool updateValue) {
    state = [
      for (int i = 0; i < state.length; i++)
        i == index ? updateValue : state[i],
    ];
  }
}

class CheckboxListSample extends ConsumerWidget {
  CheckboxListSample({Key? key}) : super(key: key);
  final List<String> _valueList = ['A', 'B', 'C', 'D'];

  
  Widget build(BuildContext context, WidgetRef ref) {
    final checkedList = ref.watch(checkedListProvider);
    return Scaffold(
      appBar: AppBar(
          title: const Text("StateNotifierProvider Sample")),
      body: Center(
        child: ListView.separated(
          itemBuilder: (context, index) => CheckboxListTile(
            title: Text(_valueList[index]),
            subtitle: Text(checkedList[index] ? "ON" : "OFF"),
            value: checkedList[index],
            onChanged: (bool? checkedValue) {
              ref
                  .watch(checkedListProvider.notifier)
                  .updateListElement(index, checkedValue!);
            },
          ),
          separatorBuilder: (context, index) {
            return const Divider(height: 0.5);
          },
          itemCount: _valueList.length,
        ),
      ),
    );
  }
}

参考記事

参考記事
GitHubで編集を提案

Discussion