[Flutter] 状態管理ライブラリ”provider”を使ってRadioボタンを別Widget化する

2022.11.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

前回のエントリではFlutterでRadio classをラジオボタンを実装してみました。

しかし同じWidget内にRadio classをそのまま記述すると可読性が悪くなってくるので部品化をしたいですね。

そこで今回は、Flutterアプリの実装で状態管理ライブラリproviderを使いつつRadioボタンを別Widget化して再利用できるようにしてみました。

providerとは

providerはInheritedWidgetクラスのラッパーで、いわゆる状態管理ライブラリです。

InheritedWidgetを使うとWidget Treeでの情報伝搬を効率的に実装できますが、providerを使用するとより簡単に実装をすることができます。

providerはFlutter Favorite programの対象となっており、Flutterアプリケーションを構築する際に最初に導入を検討するべきライブラリとされています。

やってみた

providerをインストールします。

flutter pub add provider

providerおよびRadioクラスを使用して次のように実装します。providerではプロバイダーを使って状態管理を実装します。

lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

//プロバイダーを作成
class CityProvider extends ChangeNotifier {
  City value = City.ueno; //状態の初期値

  //プロバイダーで管理している状態を更新するメソッド
  void changeCity(City newValue) {
    value = newValue;
    notifyListeners(); //状態更新を伝搬させる
  }
}

void main() => runApp(
      //プロバイダーの状態管理の範囲を定義
      MultiProvider(
        providers: [
          ChangeNotifierProvider(create: (_) => CityProvider()),
        ],
        child: const MyApp(),
      ),
    );

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Center(
          child: MyStatefulWidget(),
        ),
      ),
    );
  }
}

enum City {
  ueno('上野'),
  shinjuku('新宿'),
  akihabara('秋葉原'),
  ikebukuro('池袋'),
  shibuya('渋谷');

  final String displayName;
  const City(this.displayName);
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({super.key});

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: const <Widget>[
        RadioWidget(radioValue: City.ueno),
        RadioWidget(radioValue: City.shinjuku),
        RadioWidget(radioValue: City.akihabara),
        RadioWidget(radioValue: City.ikebukuro),
        RadioWidget(radioValue: City.shibuya),
      ],
    );
  }
}

class RadioWidget extends StatefulWidget {
  final City radioValue;
  const RadioWidget({super.key, required this.radioValue});

  @override
  State<RadioWidget> createState() => _RadioWidget();
}

class _RadioWidget extends State<RadioWidget> {
  @override
  Widget build(BuildContext context) {
    //context.watchによりプロバイダーの更新を監視し、更新時に反映する
    CityProvider cityProvider = context.watch<CityProvider>();

    return ListTile(
      title: Text(widget.radioValue.displayName),
      leading: Radio<City>(
        value: widget.radioValue,
        groupValue: cityProvider.value,
        onChanged: (City? value) {
          setState(() {
            //プロバイダーの状態を更新する
            cityProvider.changeCity(value!);
          });
        },
      ),
    );
  }
}

_RadioWidgetで自身はcontext.watchによりプロバイダーの状態更新を監視し、かつonChangedで選択変更時にchangeCity()を実行して状態を更新して、他のWidgetに状態更新を伝搬させます。

アプリを起動すると、ラジオボタンがちゃんと動作していますね。良さそうです。

ハマったこと

Error: Could not find the correct Provider above this RadioWidget Widget

当初、runApp()でのアプリ実行を次のように実装したところ、アプリ実行時にエラーが発生しました。

lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CityProvider extends ChangeNotifier {
  City value = City.ueno;

  void changeCity(City newValue) {
    value = newValue;
    notifyListeners();
  }
}

void main() => runApp(const MyApp());

RadioWidgetの上位にProvider<CityProvider>が見つからないためエラーとなっているようです。

Another exception was thrown: Error: Could not find the correct Provider above this RadioWidget Widget

次のようにMyApp()MultiProvider()の子要素とする必要がありました。

lib/main.dart

- void main() => runApp(const MyApp());
+ void main() => runApp(
+       MultiProvider(
+         providers: [
+           ChangeNotifierProvider(create: (_) => CityProvider()),
+         ],
+         child: const MyApp(),
+       ),
+     );

以降プロバイダーを追加したい場合はMultiProvider.providersにプロバイダーを追加していけばOKです。

void main() => runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider(create: (_) => CityProvider()),
          ChangeNotifierProvider(create: (_) => HogeProvider()),
          ChangeNotifierProvider(create: (_) => FugaProvider()),
        ],
        child: const MyApp(),
      ),
    );

以上