24
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FlutterのnotifyListenersとChangeNotifierProviderを整理する

Posted at

:book: Flutterの記事を整理し本にしました :book:

  • 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
  • 今後はこちらを最新化するため、最新情報はこちらをご確認くださ
  • 10万文字を超える超大作になっています(笑)

はじめに

  • いままで何となくにしていたInheritedWidgetを学びなおしたので、整理して記事にしてみました。

まとめ

ProviderとChangeNotifier

本チャプターは、下記のGoogle I/O'19の情報をベースとしています。

InheritedWidgetを使うと上位のWidgetの値を使えますが、InheritedWidgetを直接使うことは少なく、Providerという便利なパッケージがあるので、こちらを使うことが多いです。

InheritedWidgetについて、よくわからないという方は、InheritedWidgetをご参照ください

Providerは、InheritedWidgetラッパーライブラリです。
使う場合は、ライブラリの読み込みが必要です。

pubspec.yaml
dependencies:
  provider: "^5.0.0"

使い方はInheritedWidgetに似ており、InheritedWidgetの代わりに、使うWidgetの上位にProviderを挟んでおき、下位Widgetでその値を使います。

前回のInheritedWidgetとの対比で確認してみます。

main.dart
import 'package:flutter/material.dart';
import 'package:hello_world/Widgets.dart';
import 'package:provider/provider.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);
  final String? title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    print("count:" + _counter.toString());
  }
  // Scaffoldの下のCenter部分を先に静的に作っておき、作り返さないように制御
  // 深い階層の伝播は証明できたためにシンプルにCenter->WidgetAに変更
  final Widget _widget = Center(child: WidgetA());
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
-      //body: MyInheritedWidget(count: _counter, child: _widget),
+      body: Provider<int>.value(value: _counter, child: _widget),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
WidgetA.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int count;
    try {
-      //MyInheritedWidget _myInheritedWidget = MyInheritedWidget.of(context);
-      //count = _myInheritedWidget.count;
+      count = Provider.of<int>(context);
    } catch (e) {
      count = 0;
    }
    return Text("$count", style: TextStyle(fontSize: 100));
  }
}

ほぼInheritedWidgetProviderに置き換わっただけであることが確認できます。

pic4.png

Consumer

前回のようにWidgetAを自作せずに、Providerパッケージが提供するConsumerを使うとより簡単に同じ機能を実現できます。

main.dart
// _widgetの定義以外は同じのため省略
- final Widget _widget = Center(child: WidgetA());
+ final Widget _widget = Center(
+       child: Consumer<int>(
+     builder: (context, value, _) => Text(
+      value.toString(),
+      style: TextStyle(fontSize: 100),
+    ),
  ));

もちろん、再構築される範囲はこのConsumerの範囲のみです。

Providerにはいくつかの種類が提供されています。
この後紹介する変更を検知して動作するChangeNotifierProviderや複数のProviderを取り扱えるmultiProviderなどがあります

ChangeNotifierとChangeNotifierProvider

Providerの上位Widgetの値の取得を発展させ、データが変わったことを通知する(notifyListeners)とデータが変わったことを知りWidgetを作り直す(ChangeNotifierProvider)を使って、状態管理を行うことができます。

以下のようなスライダーとその値を表示させるシンプルなアプリで動作を確認します。

Screenshot_1618644532.png

MyData.dart
import 'package:flutter/foundation.dart';
class MyData with ChangeNotifier {
  double _value = 0.5;
  // getter
  double get value => _value;
  // setter
  set value(double value) {
    _value = value;
    notifyListeners(); //通知
  }
}

まず、データを保持し、変更されたら通知するクラスを作ります。
ChangeNotifierwithmixinすることで、通知を行うnotifyListenersが使えるようになります。
この関数が呼ばれると、変更を監視しているWidgetに変更が通知されます。

Slider.dart
import 'package:flutter/material.dart';
import 'package:hello_world/mydata.dart';
import 'package:provider/provider.dart';
class MySlider extends StatefulWidget {
  @override
  createState() => _MySliderState();
}
class _MySliderState extends State<MySlider> {
  @override
  Widget build(BuildContext context) {
    final mydata = Provider.of<MyData>(context);
    return Slider(
        value: mydata.value, onChanged: (value) => mydata.value = value);
  }
}

次にスライダーです。
スライダーで設定した値をMyDataに設定します。
また、スライダの位置をvalueで設定するため、入力値としてMyDataの値の取得も行います。

main.dart
void main() { /* 変更なしのため省略 */}
class MyApp extends StatelessWidget { /* 変更なしのため省略 */}
class MyHomePage extends StatefulWidget { /* 変更なしのため省略 */}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyData(),
      child: Scaffold(
          appBar: AppBar(
            title: Text(widget.title!),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Consumer<MyData>(
                builder: (context, mydata, _) => Text(
                  mydata.value.toStringAsFixed(2),
                  style: TextStyle(fontSize: 100),
                ),
              ),
              MySlider(),
            ],
          )),
    );
  }
}

ChangeNotifierProviderを上位に挟み、変更された時にその変更を検知できるようになっています。

処理の流れは下記のとおりです。

  1. SliderはProvider.ofでMyDataにアクセスし、値の取得や設定を行う
  2. MyDataのsetterの中で、notifyListenersでリスナーに変更を通知する
  3. Consumerが変更を検知して、Textへの値の設定と作り直しを行う

context.select/context.read

下記は、BuildContextのextensionとして、コードをシンプルにするために役立ちます。

  • context.select(変更を監視する)
  • context.read(変更を監視しない)
Slider.dart
import 'package:flutter/material.dart';
import 'package:hello_world/mydata.dart';
import 'package:provider/provider.dart';
class MySlider extends StatefulWidget {
  @override
  createState() => _MySliderState();
}
class _MySliderState extends State<MySlider> {
  @override
  Widget build(BuildContext context) {
    // context.select,readを使ってアクセス
    return Slider(
        value: context.select((MyData mydata) => mydata.value),
        onChanged: (value) => context.read<MyData>().value = value);
  }
}
main.dart
void main() { /* 変更なしのため省略 */}
class MyApp extends StatelessWidget { /* 変更なしのため省略 */}
class MyHomePage extends StatefulWidget { /* 変更なしのため省略 */}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyData(),
      child: Scaffold(
          appBar: AppBar(
            title: Text(widget.title!),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Consumer<MyData>(
                  // context.readを使ってアクセス
                  builder: (context, schedule, _) => Text(
                      context.select(
                          (MyData mydata) => mydata.value.toStringAsFixed(2)),
                      style: TextStyle(fontSize: 100))),
              MySlider()
            ],
          )),
    );
  }
}

Providerパッケージをより改善したRiverpodパッケージもありますが、今回は取り扱いません。
「グローバルでの定義が可能」「ランタイムエラーを起こさない」「同じ型での複数探索が可能」など改良が加えられています。

Providerデザインパターン

本チャプターで見てきた内容はProviderデザインパターンと呼ばれるものです。

開発の中級4:StreamとBLoCデザインパターンでは、Streamというものを用いて、入力、ビジネスロジック、出力の3つを分離するデザイパターンを紹介しました。

こちらは、Streamを使ってビジネスロジックを分離する考え方でしたが、Providerデザインパターンは、データの変更をnotifyListnersで通知し、ChangeNotifierProviderでその変更を検知することで、入出力とビジネスロジックを分離します。

整理すると、下記のようになります。

pic7.png

上記の整理は基本的な考え方に基づくもので、現実的には組み合わせたりプロジェクトの案件に合わせてカスタマイズされることが多くあります。

24
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?