😽

【めっちゃシンプル】公式も紹介する状態管理であるProviderパッケージの使い方

に公開

はじめに

みなさん、こんにちは。

今回はFlutterの状態管理といえば!の第一の選択肢になるproviderパッケージの使い方についてまとめています。

これから学習する方の理解の手助けになれば幸いです。

サンプルコードのリポジトリ

https://github.com/peter-norio/flutter/tree/main/provider_sample_lib

動作画面

providerパッケージとは

概要

  • 状態管理パッケージの1つ
  • Flutter公式でも紹介している
  • シンプルな記述が可能なInheritedWidgetのラッパー
  • Remi Rousselet氏によって開発

状態管理パッケージの1つ

状態管理パッケージとは、Flutter アプリケーションにおいて、UI の状態(データ)を管理し、その状態の変化に応じて UI を更新するための仕組みを提供するパッケージのことです。

状態の変化の監視と更新を行います。provider パッケージは、状態の変化を監視し、その状態に依存するウィジェットを自動的に更新する仕組みを提供します。これにより、開発者は UI の更新処理を自分で実装する必要がなくなります。また、provider パッケージは、必要なウィジェットのみを再構築することで、パフォーマンスを最適化します。

Flutter公式でも紹介している

provider パッケージは、Flutter 公式ドキュメントで状態管理の方法として紹介されており、Flutter 開発において広く利用されている、推奨される状態管理パッケージの一つです。

https://docs.flutter.dev/data-and-backend/state-mgmt/simple#changenotifierprovider

シンプルな記述が可能なInheritedWidgetのラッパー

provider パッケージは、Flutter の状態管理の基本的な仕組みである InheritedWidget を使いやすくするためのラッパーとして機能します。

provider パッケージは、状態管理を容易にするための API を提供します。これにより、InheritedWidget を直接使用する場合に比べて、コード量を大幅に削減できます。

Remi Rousselet氏によって開発

provider は、Remi Rousselet 氏によって開発されたパッケージです。

provider は、Remi Rousselet 氏によって比較的早い時期に開発されたパッケージです。Flutter がまだ初期の段階だった頃から、状態管理の方法として provider は存在していました。Flutter の状態管理の初期段階において、provider は比較的シンプルで使いやすいライブラリとして、多くの開発者に利用されています。

providerパッケージの導入

概要

  • flutter pub add providerで導入
  • コード上ではインポートして利用

flutter pub add providerで導入

flutter pub add provider は、Flutter プロジェクトに provider パッケージを簡単に追加するためのコマンドです。ターミナルまたはコマンドプロンプトで、Flutter プロジェクトのルートディレクトリに移動し、コマンドを実行します。

コード上ではインポートして利用

provider パッケージをプロジェクトに追加したら、Dart コード内で provider パッケージの機能を使用するために、import 文で provider パッケージをインポートします。

import 'package:provider/provider.dart';

これで、provider パッケージのクラスやメソッドをコード内で使用できるようになります。

providerでの状態管理の大枠

概要

  • 状態クラスで状態の定義
  • 状態をアプリ内に共有
  • 各ウィジェットから状態を利用する

状態クラスで状態の定義

アプリケーションで管理したいデータやロジックを含むクラスを作成します。このクラスはChangeNotifierを継承し、データの変更時にリスナーへ通知を行うことで、UIの再構築を促します。

状態をアプリ内に共有

作成した状態クラスをアプリケーション全体、または特定のウィジェットツリー内で共有できるように、ChangeNotifierProviderを用いてプロバイダとして提供します。これにより、子ウィジェットはプロバイダを通じて状態にアクセスできるようになります。

各ウィジェットから状態を利用する

子ウィジェット内でcontext.watchなどを使用して、プロバイダから状態を取得し、データの表示やユーザー操作に応じた状態の更新を行います。これにより、状態の変更が自動的にUIに反映されます。

状態クラスの作成

概要

  • 状態クラスでを状態と操作を定義
  • 機能単位で状態クラスを作成

状態クラスでを状態と操作を定義

ChangeNotifierを継承して状態クラスを作成します。ChangeNotifierは、状態が変更されたときにウィジェットに通知する仕組みを持っています。

状態クラス内では次のものを定義します。

  • プライベートフィールド: 状態として管理したいデータを定義します。通常はアンダースコア _ で始まるプライベート変数として定義し、外部からの直接的な変更を防ぎます。
  • ゲッター : 外部から状態を読み取るためのゲッターを提供します。これにより、状態の読み取り専用アクセスを制御できます。
  • 状態を変更するメソッド : 状態を変更するためのメソッドを定義します。状態が変更された後に必ず notifyListeners() を呼び出します。これにより、この ChangeNotifier を監視しているウィジェットが再描画され、新しい状態が反映されます。

買い物かごを表す状態クラスの例(lib/states/cart_state.dart)
この状態クラスは1つの状態、3つの読み取り可能なデータ、1つの操作メソッドを持っています。

  • 状態:カート内の商品リスト
  • データ:カート内の商品リスト、 カート内の商品数量、カート内の商品合計金額
  • 操作メソッド:カートに商品を追加する
import 'package:flutter/material.dart';
import 'package:provider_sample/models/item.dart';

// 買い物かごを表す状態クラス
class CartState extends ChangeNotifier {
  // 状態データを定義
  // カート内の商品リストを状態として定義
  // 状態クラスのフィールドは _ で宣言
  final List<Item> _cart = [];

  // カート内の商品リストを取得するgetter
  List<Item> get cart => _cart;

  // カート内の商品数量を取得するgetter
  int get amount => _cart.length;

  // カート内の商品合計金額を取得するgetter
  int get totalPrice {
    int total = 0;
    // カート内の商品をループして合計金額を計算
    for (final item in _cart) {
      total += item.price;
    }
    return total;
  }

  // カートに商品を追加するメソッド
  void addItem(Item item){
    _cart.add(item);
    notifyListeners();
  }
}

機能単位で状態クラスを作成

状態クラスを作成する単位は機能単位と考えることができます。商品一覧、買い物かごなど、アプリケーションの各機能ごとに、その機能に必要な状態を管理する専用のクラスを作成します。

1つのクラスに詰め込まず、分割されることで、関心の分離を高め、コードの可読性・保守性を向上させます。また、特定の機能の状態変更が他の機能に影響を与えにくくなります。

ストアを複数用意するイメージです。アプリケーション全体で、複数の独立した状態管理の単位(ストア)を持つ構成とします。各ストアは特定の機能や画面の状態を管理します。

参考リンク集

状態の共有を開始する

概要

  • ChangeNotifierProvider で状態の共有を開始する
  • 複数の状態クラスを利用する場合はMultiProviderを利用する

ChangeNotifierProvider で状態の共有を開始する

作成した状態クラスのインスタンスを、ウィジェットツリーの上位でChangeNotifierProviderを使って提供します。通常は、アプリケーションのルートや、状態を共有したいウィジェットツリーの最も上位の親ウィジェットの直下で行います。

ChangeNotifierProviderProviderウィジェットの一種で、ChangeNotifierのインスタンスを提供するために使用します。

create 引数には、状態クラスのインスタンスを返す関数を指定します。この関数は BuildContext を引数に取り、プロバイダーが初めて必要とされたときに一度だけ実行されます。

child引数には状態を共有するウィジェットツリーのルートとなるウィジェットを指定します。このウィジェットとその子孫ウィジェットは、このプロバイダーによって提供される状態にアクセスできます。

ルートを指定しアプリ全体に状態を共有する例(lib/main.dart)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_sample/home_screen.dart';
import 'package:provider_sample/states/cart_state.dart';

// アプリのエントリーポイント
void main() {
  runApp(
    // プロバイダーを通して状態を共有
    ChangeNotifierProvider(
      // 状態オブジェクトを返す関数を指定
      create: (context) => CartState(),
      // 状態を共有するウィジェット(ルートを指定)
      child: const MyApp()),
  );
}

// ルートウィジェット
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: HomeScreen(),
    );
  }
}

複数の状態クラスを利用する場合はMultiProviderを利用する

MultiProvider は、複数の状態クラスのインスタンスをまとめて提供するためのProviderです。これにより、複数の独立した状態やサービスを、ウィジェットツリーの特定の部分で簡単に利用できるようになります。

複数の状態クラスを共有する例

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartState()),
        ChangeNotifierProvider(create: (context) => SomeState()),
      ],
      child: const MyApp(),
    ),
  );
}
参考リンク集

状態を利用する

概要

  • context.watch メソッドで状態の値利用と変更に反応
    • context.selectは指定された一部の値の変更のみを監視
  • context.read メソッドで状態の一時的な読み取り
  • 状態クラスのインスタンスから状態更新用のメソッドを呼び出して更新

context.watch メソッドで状態の値利用と変更に反応

context.watch<T>() は、Flutter の provider パッケージが提供する BuildContext の拡張メソッドの一つです。このメソッドを使用すると、指定した状態クラスの値(状態)を監視し、その値が変更された場合に自身を再描画します。

context.watch() メソッドを使用する際には、ジェネリクス <T> を用いて、監視したい状態クラスの型を明示的に指定する必要があります。

買い物かごの商品数量(状態)を表示する例

// カート内の商品数量を表示
// 状態の変更に応じて表示を変化させるためwatchで監視
Text('カート内商品${context.watch<CartState>().amount}個'),

context.read メソッドで状態の一時的な読み取り

context.watch<T>() とは異なり、context.read<T>() を呼び出したウィジェットは、その時点での状態の値を取得するだけで、その後の値の変更を監視しません。

状態の変更に反応する必要がない場合に適しています。主に状態クラスのインスタンスを取得するのに利用し、状態更新ロジックの呼び出しなどを行います。イベントハンドラー(ボタンの onPressed コールバックなど)の中で、現在の状態に基づいて何らかの処理を行いたい場合に利用されます。

状態更新ロジックを呼び出す例

// 状態クラスから提供される状態操作メソッドを利用
context.read<CartState>().addItem(newItem);

状態クラスのインスタンスから状態更新用のメソッドを呼び出して更新

状態クラスは、内部に管理している状態(変数)を変更するためのメソッドを通常持ちます。これらのメソッドをウィジェットで呼び出して状態更新を行います。

ホーム画面で買い物かごの情報の表示と更新をしてる例(lib/home_screen.dart)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_sample/models/item.dart';
import 'package:provider_sample/states/cart_state.dart';
import 'package:provider_sample/widgets/shopping_app_bar.dart';

// ホーム画面を表すウィジェット
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  // カートに商品を追加するメソッド
  // メソッドに切り出した理由:複雑な処理をウィジェットの配置内から分離して可読性を高めるため
  void _addToCart(BuildContext context) {
    // 新しく追加するItemオブジェクトを用意
    final newItem = Item(
      // idやnameは連番にしたいので、現状カート状態の要素数+1とする
      // readにしてる理由:状態の変更を監視する必要がなく、現時点の値が取得できれば良いため
      id: context.read<CartState>().amount + 1,
      name: '商品${context.read<CartState>().amount + 1}',
      price: 100,
    );
    // 買い物かごに商品を追加する
    // 状態クラスから提供される状態操作メソッドを利用
    context.read<CartState>().addItem(newItem);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      // 別ファイルに切り出したAppBarウィジェットを配置
      appBar: ShoppingAppBar(),
      // 画面中央にメインコンテンツを配置
      body: Center(
        child: Column(
          // 子ウィジェットを中央に配置
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // カート内の商品数量を表示
            // 状態の変更に応じて表示を変化させるためwatchで監視
            Text('カート内商品${context.watch<CartState>().amount}個'),
            // context.selectで数量を監視する例
            Text('カート内商品${context.select<CartState, int>((cart)=>cart.amount)}個'),
            const Text('商品リスト', style: TextStyle(fontSize: 24)),
            // 商品追加のボタン
            ElevatedButton(
              // ボタン押下時に商品追加の関数を実行
              onPressed: () {
                _addToCart(context);
              },
              child: const Text('商品を追加'),
            ),
            Padding(
              padding: const EdgeInsets.all(16.0),
              // カート内の合計金額の表示
              child: Text(
                // 状態の変更に伴い表示の更新をするためwatchで監視
                '合計金額: ${context.watch<CartState>().totalPrice}円',
                style: const TextStyle(fontSize: 20),
              ),
            ),
            // 商品リストを表示
            Expanded(
              child: Column(
                // childrenにmapの結果を直接指定し、繰り返し表示
                // 繰り返しの基準は状態としてるカートのリスト
                // リストの変化(商品の追加)に応じて画面を更新するのでwatchで監視
                children:
                    context
                        .watch<CartState>()
                        .cart
                        .map((item) => Text('商品名:${item.name}'))
                        .toList(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

カスタムアップバーで買い物かごの情報を表示する例(lib/widgets/shopping_app_bar.dart)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_sample/states/cart_state.dart';

// 買い物かご画面で表示するAppBarウィジェット
// AppBarを切り出してウィジェット化する場合は、PreferredSizeWidgetインターフェースを実装する
class ShoppingAppBar extends StatelessWidget implements PreferredSizeWidget {
  const ShoppingAppBar({super.key});

  // PreferredSizeWidgetインターフェースのメソッド
  // overrideの内容は気にせずこれにする
  
  Size get preferredSize => Size.fromHeight(kToolbarHeight);

  
  Widget build(BuildContext context) {
    return AppBar(
      // AppBarのタイトル
      title: Text('Shopping Cart'),
      // 右端に買い物かごアイコンと数量を表示する
      actions: [
        Padding(
          padding: EdgeInsets.all(8.0),
          // 横に並べる
          child: Row(
            children: [
              // 買い物かごアイコン
              Icon(Icons.shopping_cart),
              // カート内の商品数
              // CartStateが提供する数量のgetterを利用
              Text('${context.watch<CartState>().amount}'),
            ],
          ),
        ),
      ],
    );
  }
}
参考リンク集

おわりに

Flutterの状態管理としてproviderパッケージの使い方を紹介しました。

providerの記述はシンプルでとても分かりやすいと思います。その一方で状態クラスの存在が分かりづらさを誘発しているかもしれないと感じます。Flutterは良くも悪くもすべてクラスで表現するので、各クラスの役割を明確に理解することが重要だと考えます。

Discussion