[Flutter] providerでのcontextの読み取り方法3種類を使い比べてみる

2022.11.20

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

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

以前のエントリでFlutterの状態管理ライブラリproviderを使用して、あるWidgetでの値の変更を別のWidgetに伝搬させる実装を行いました。

providerでは、状態として管理している値(context)を読み取る方法は次の3種類が提供さています。

  • context.watch<T>()
  • context.read<T>()
  • context.select<T, R>(R cb(T value))

今回は、providerでのcontextの読み取り方法3種類を使い比べてみました。

やってみた

context.watch

context.watchを使うと、プロバイダーの値の更新を監視して、更新時にWidgetに反映することができます。

以前のエントリでFlutterの状態管理ライブラリproviderで作成したコードほぼそのままですが、context.watchCityProviderの更新を読み取るようにしています。

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),
        SizedBox(
          height: 30,
        ),
        SelectedCityText()
      ],
    );
  }
}

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) {
    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!);
          });
        },
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.watch<CityProvider>();
    return Text('現在は ${cityProvider.value.displayName} が選択されています');
  }
}

context.read

context.readを使うと、プロバイダーの値をWidgetの初回レンダリング時にのみ読み取ってWidgetに反映させることができます。

先程のコードでcontext.watchcontext.readに置き換えます。

lib/main.dart

class _RadioWidget extends State<RadioWidget> {
  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.read<CityProvider>();

    return ListTile(
      title: Text(widget.radioValue.displayName),
      leading: Radio<City>(
        value: widget.radioValue,
        groupValue: cityProvider.value,
        onChanged: (City? value) {
          setState(() {
            cityProvider.changeCity(value!);
          });
        },
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.read<CityProvider>();
    return Text('現在は ${cityProvider.value.displayName} が選択されています');
  }
}

するとラジオボタンを操作してCityProviderを更新しても、他(および自身)のWidgetに値の更新が反映されなくなりました。

context.select

context.selectを使うと、プロバイダーで管理している一部値のみを監視して、更新時にWidgetに反映することができます。

context.selectを使わない場合

まずcontext.selectを使わずにcontext.watchをそのまま使い続けた場合です。

コードを次のように修正します。CityProviderに2つ目の値isCheckedを追加し、isCheckedを更新するWidget_CheckBoxを追加しています。

lib/main.dart

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

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

  bool isChecked = false;

  void changeIsChecked(bool newValue) {
    isChecked = newValue;
    notifyListeners();
  }
}

lib/main.dart

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),
        SizedBox(
          height: 30,
        ),
        SelectedCityText(),
        SizedBox(
          height: 30,
        ),
        CheckBox()
      ],
    );
  }
}

lib/main.dart

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

  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.watch<CityProvider>();
    print('_RadioWidget has rendered.'); //ビルドされたことをデバッグコンソールに表示
    return Text('現在は ${cityProvider.value.displayName} が選択されています');
  }
}

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

  @override
  State<CheckBox> createState() => _CheckBox();
}

class _CheckBox extends State<CheckBox> {
  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.watch<CityProvider>();
    print('_CheckBox has rendered.'); //ビルドされたことをデバッグコンソールに表示

    return CheckboxListTile(
      title: const Text('チェックボックス'),
      value: cityProvider.isChecked,
      onChanged: (bool? value) {
        setState(() {
          cityProvider.changeIsChecked(value!);
        });
      },
    );
  }
}

ここでCityProviderの管理している値のうち、_RadioWidgetvalueのみ、_CheckBoxisCheckedのみを使用しています。

しかしデバッグコンソールを見ながらWidgetを操作してCityProviderの値を変更すると、それぞれのWidgetを操作する毎に、すべてのWidgetで再ビルドが走っていますね。

しかしこれだと無駄なビルドが行われてしまっているので改善の余地がありそうです。

context.selectを使った場合

次にcontext.selectを使用します。CityProviderで管理している値はcontext.select((CityProvider cityProvider) => cityProvider.値)のようにして監視が必要な値のみ読み取るようにし、またCityProviderのメソッドはcontext.read<CityProvider>()によりWidget内で使用できるようにします。

lib/main.dart

class _RadioWidget extends State<RadioWidget> {
  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.read<CityProvider>();
    final value =
        context.select((CityProvider cityProvider) => cityProvider.value);

    return ListTile(
      title: Text(widget.radioValue.displayName),
      leading: Radio<City>(
        value: widget.radioValue,
        groupValue: value,
        onChanged: (City? value) {
          setState(() {
            cityProvider.changeCity(value!);
          });
        },
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final value =
        context.select((CityProvider cityProvider) => cityProvider.value);
    print('_RadioWidget has rendered.'); //レンダリングされたことをDebugコンソールに表示
    return Text('現在は ${value.displayName} が選択されています');
  }
}

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

  @override
  State<CheckBox> createState() => _CheckBox();
}

class _CheckBox extends State<CheckBox> {
  @override
  Widget build(BuildContext context) {
    CityProvider cityProvider = context.read<CityProvider>();
    final isChecked =
        context.select((CityProvider cityProvider) => cityProvider.isChecked);
    print('_CheckBox has rendered.'); //レンダリングされたことをDebugコンソールに表示

    return CheckboxListTile(
      title: const Text('チェックボックス'),
      value: cityProvider.isChecked,
      onChanged: (bool? value) {
        setState(() {
          cityProvider.changeIsChecked(value!);
        });
      },
    );
  }
}

デバッグコンソールを見ながらWidgetを操作してみると、必要最低限のWidgetの再ビルドのみ行われるようになっていますね。

まとめ

  • context.watch<T>():プロバイダーで管理しているすべての値およびメソッドを監視し、読み取る場合に使用する。
  • context.read<T>():プロバイダーで管理している値の初期値またはメソッドのみを読み取る場合に使用する。
  • context.select<T, R>(R cb(T value)):プロバイダーで管理している一部の値のみ監視し、読み取る場合に使用する。

参考

以上