[Flutter] Provider

2021-05-21 hit count image

今回のブログポストではFlutterでグローバル状態(State)、またはウィジェットたちの間で状態(State)を共有するためProviderを使う方法について説明します。

概要

今回ブログポストではFlutterでグローバル状態(State)を管理するため、またはウィジェットたちの間で状態(State)を共有するため使えるProviderについて説明します。

このブログポストで紹介するソースコードはGitHubで確認できます。

それじゃ、FlutterでProviderを使ってグローバル状態管理やウィジェットたちの間で状態を共有する方法についてみてみましょう。

Flutterプロジェクト生成

次のコマンドを実行してflutter_providerパッケージを使うFlutterプロジェクトを生成します。

flutter create provider_example

Null safetyを適用するため次のコマンドを実行します。

cd provider_example
dart migrate --apply-changes

flutter_providerパッケージのインストール

Flutterでグローバル状態管理やウィジェットたちの間で状態を共有するため、次のコマンドを実行してflutter_providerパッケージをインストールします。

flutter pub add flutter_provider

Providerとは

Flutterでは大きく2種類のウィジェットが存在します。1は状態を持ってないStateless Widgetと状態(State)を持ってるStateful Widgetです。

Statefule Widgetは1つのウィジェット中で状態(データ)を持ってその状態の変化によって画面に表示されたUIを変更します。

flutter state

もし、他のウィジェットで同じ状態(データ)が必要な場合はどうすれば良いでしょうか?

flutter state required

状態をシェアする2つの共通親ウィジェットをStateful Widgetを作って、チャイルドウィジェットを生成する時、パラメータでその状態を渡して、2つのウィジェットの間で同じ状態を使うことができます。

flutter state required

しかし、状態を表示するため要らないウィジェットたちがRe-buildされ性能的な問題が出る可能性があります。Providerはこの問題を解決するため登場しました。このように同じ状態(データ)をグローバル的他のウィジェットと共有する時、Providerを使います。

flutter provider

Providerを使う時には、ウィジェットツリーと関係なく状態(データ)を保存するクラスを生成して、当該状態をシェアする共通親ウィジェットにProviderを提供(Provide)して、状態を使うところでProviderのデータを読み込んで使います。

使い方

次はflutter_providerを使って実際グローバルの状態を管理してみましょう。今回のブログポストではflutter_providerパッケージを使って、次のような簡単なカウンターアプリを開発する予定です。

flutter provider

+ボタンを押すと、画面に表示される数字が上がって、-ボタンを押すと数字が下がる単純なアプリです。このアプリでFlutterでProviderを使う方法について説明します。

Provider

まず、グローバルデータを管理するためProviderを作ってみましょう。lib/providers/counts.dartファイルを生成して次のように修正します。

import 'package:flutter/material.dart';

class Counts with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void add() {
    _count++;
    notifyListeners();
  }

  void remove() {
    _count--;
    notifyListeners();
  }
}

Providerを使うためにはChangeNotifierを使ってクラスを生成する必要があります。

import 'package:flutter/material.dart';

class Counts with ChangeNotifier {
  ...
}

そしてアプリ内で共有する状態変数を宣言します。また、その変数を外部からアクセスできるようにgetterも生成します。

import 'package:flutter/material.dart';

class Counts with ChangeNotifier {
  int _count = 0;
  int get count => _count;
  ...
}

その後、その状態変数を変更する関数を生成します。ここでは値を上げるadd関数と値を下げるremove関数を生成しました。

class Counts with ChangeNotifier {
  ...
  void add() {
    _count++;
    notifyListeners();
  }

  void remove() {
    _count--;
    notifyListeners();
  }
}

ここで重要なものは変数を修正したら、notifyListeners()を実行して、データが更新されたことを知らせます。Stateful Widgetで値が変わったことを知らせるためにsetState関数を使うことと同じです。notifyListeners関数を実行しないと、他のウィジェットで値が変更されたことが認識できないです。

これでProviderを使ってアプリ全体で使うグローバル状態を生成しました。

Main

次は生成したグローバル状態を使うウィジェットたちの共通親ウィジェットにProviderを提供してみましょう。lib/main.dartファイルを開いて下記のように修正します。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_example/providers/counts.dart';
import 'package:provider_example/widgets/buttons.dart';
import 'package:provider_example/widgets/counter.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counts()),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider'),
      ),
      body: ChangeNotifierProvider(
        create: (BuildContext context) => Counts(),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Counter(),
              Buttons(),
            ],
          ),
        ),
      ),
    );
  }
}

上で生成したグローバル状態を使うためflutter_proivderパッケージと状態クラスをインポートしました。

...
import 'package:provider/provider.dart';
import 'package:provider_example/providers/counts.dart';
import 'package:provider_example/widgets/buttons.dart';
import 'package:provider_example/widgets/counter.dart';

まだ、作ってはないですが、Providerを使うウィジェットも追加しました。

...
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counts()),
      ],
      child: MyApp(),
    ),
  );
}
...

今回の例題では一番上のウィジェットにProviderを提供しました。普通は1つのアプリを開発する時、1つ以上のProviderを使うので、この例題ではMultiProviderを使って複数のProviderを提供できるようにしました。

...
class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider'),
      ),
      body: ChangeNotifierProvider(
        create: (BuildContext context) => Counts(),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Counter(),
              Buttons(),
            ],
          ),
        ),
      ),
    );
  }
}
...

後は、普通のアプリを開発する方法で画面を構成しました。次はProviderを使うウィジェットであるCounterButtonsを開発してみましょう。

Counter

そしたら、Providerを使うウィジェットを作ってみましょう。lib/widgets/counter.dartファイルを生成して次のように修正します。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_example/providers/counts.dart';

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('Counter');

    return Text(
      context.watch<Counts>().count.toString(),
      style: TextStyle(
        fontSize: 20,
      ),
    );
  }
}

CounterウィジェットはTextウィジェットを使って、画面に数字を表示する単純なウィジェットです。この時、context.watch<Counts>().countを使って私たちが作ったProviderのcountの値が変更されることを監視して、変更がある場合その変更された値を表示するようにしました。

Buttons

次は、私たちが作ったProviderの値を変更するため、Buttonsウィジェットを作ってみましょう。lib/widgets/buttons.dartファイルを生成して次のように修正します。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_example/providers/counts.dart';

class Buttons extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
            onPressed: () {
              context.read<Counts>().add();
            },
            child: Icon(Icons.add)),
        SizedBox(
          width: 40,
        ),
        ElevatedButton(
            onPressed: () {
              context.read<Counts>().remove();
            },
            child: Icon(Icons.remove))
      ],
    );
  }
}

Counterウィジェットとは違ってButtonsウィジェットではProviderのcountを変更するためcontext.read<Counts>()を使ってaddremove関数をコールしました。

Buttonsウィジェットではaddまたはremove関数がコールされると、Providerで当該変数を変更した後、notifyListeners()関数をコールして値が変更されたことを知らせます。このように値が変更されたら、Providerのcontext.watchまたはcontext.selectで当該値を使うウィジェットたちは値が変更でre-buildが反省してウィジェットが新しい値と一緒に再表示されます。

watch, read, select

Providerはwatch, read, selectの機能を提供しております。

  • read: 当該ウィジェットは状態の値を読み込みます。しかし、変更を監視しません。
  • watch: 当該ウィジェットが状態の値の変更を監視します。
  • select: 当該ウィジェットは状態の値の特定な部分だけ監視します。

普通Providerの値を変更するための関数はreadを使ってアクセスするし、状態の値を使う時にはwatchを使います。変更された状態の値を表示するためにはre-buildが発生しますが、このre-buildは費用が高いです。したがって、次のようにselectを使って特定な値の変更だけ監視してre-buildを最適化することができます。

Widget build(BuildContext context) {
  final name = context.select((Person p) => p.name);
  return Text(name);
}

実行

今まで開発したFlutterアプリを次のコマンドで実行します。

flutter run

または使っているエディターのデバッグ機能を使って実行すると、次のような画面が見えます。

flutter provider

そして画面に見える+ボタンを押したら表示された数字が上がることが確認できます。また、-ボタンを押すと表示された数字が下がることが確認できます。

完了

これでFlutterでProviderを使って色んなウィジェットで使えるグローバル状態を管理する方法についてみてみました。また、簡単な例題を使ってProviderを使う方法についてもみてみました。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

Posts