【Flutter/Dart】Providerで状態管理を実装する方法!MVVM設計での活用

【Flutter/Dart】Providerで状態管理を実装する方法!MVVM設計での活用

この記事からわかること

  • Flutter/DartProvider使い方
  • 状態管理実装方法
  • ChangeNotifierMultiProviderConsumerとは?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Providerとは?

公式リファレンス:Provider

Provider」はFlutterでWidget間でデータ(状態)を共有する仕組みを提供するパッケージです。FlutterにはInheritedWidgetという組み込みの状態管理機能が存在していますが、処理が煩雑で少し使いづらさがあります。このInheritedWidgetをラッパーして使いやすくしているのが「Provider」になります。

Provider」を使用するメリットは「無駄な再描画(リビルド)を減らしパフォーマンスを向上できる」ことと「親Widgetと子Widgetでのデータの受け渡しが簡単になる」ことかなと思います。これはStatefulWidgetを使わずStatelessWidgetでWidgetを実装できる点も大きく関わっています。StatefulWidgetではsetStateメソッドを実行した際に該当のウィジェット全体がリビルドされる特徴があります。しかしこれは状態管理が増えるほどリビルドも多くなりパフォーマンスにも影響を及ぼしかねません。

ProviderではStatelessWidgetで実装でき、かつ特定のWidgetだけを更新できる仕組みによりパフォーマンスの低下を最小限に留めることが可能になります。

※ 今の主流は「Provider」ではなく上位互換の「Riverpod」です。「Riverpod」は「Provider」と同じ開発者が開発し、「Provider」の弱点を修正する形で実装された状態管理のパッケージです。

おすすめ記事:【Flutter/Dart】Riverpodの使い方!状態管理の基本

導入方法

pub.dev:Provider

FlutterでProviderを利用できるようにするために以下のコマンドを実行してパッケージを導入します。

$ flutter pub add provider

これでパッケージの導入が完了し、import文を追加すれば使用できるようになります。

import 'package:provider/provider.dart';

カウンターアプリで使い方を学ぼう

Providerの実装方法を理解するために簡単なカウンターアプリを作成してみます。その前に重要になるポイントを確認しておきます。それぞれの詳細は後述するとして全体のコードを確認してみましょう。

3つのポイント

それぞれのポイント(役割)ごとに色々種類がありますが、今回は上記を使用した場合のサンプルコードで見ていきます。全体のコードは「Github」にアップしているので参考にしてください。

1.ChangeNotifierで状態変化を通知できるクラスの実装

ChangeNotifierという状態の変化を通知することができるクラスを活用し、UI更新を行わせたいタイミングを発火できる状態にしておきます。これはViewModelなどに持たせたりすることが一般的かなと思います。

ChangeNotifierextendsする形でクラスを定義し、UI更新を行わせたいタイミングでnotifyListenersメソッドを実行するようにしておきます。


// ChangeNotifier => 状態管理を行えるリスナー付きのクラス 
// notifyListenersメソッドで変化があったことを通知する
class CounterViewModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count; // カウンターの値を取得

  void increment() {
    _count++; // カウンターを増やす
    notifyListeners(); // UIを更新
  }

  void decrement() {
    _count--; // カウンターを減らす
    notifyListeners(); // UIを更新
  }
}

2.MultiProviderで状態管理クラスの供給

続いてChangeNotifierクラスを子Widgetから参照できるようにします。ここでは複数登録に対応しているMultiProviderを使用しています。

特定の画面でのみ使用したい場合は特定のWidgetで供給する方が冗長な処理が動作しなくなります。


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Example',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MultiProvider(
        providers: [
          ChangeNotifierProvider(create: (context) => CounterViewModel()),
        ],
        child: CounterChangeNotifierView(),
      ),
    );
  }
}

3.ConsumerでUIを更新

更新された際にリアルタイムにUIの再描画処理を実行するためにConsumerを使用します。Consumer<T>形式でChangeNotifierを継承した型を指定することでその型にアクセスできるようになり、状態をViewに埋め込んでおくことでnotifyListenersが実行されたタイミングでそのWidgetだけを再描画させることができます。


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

class CounterChangeNotifierView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter with Provider')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter Value', style: TextStyle(fontSize: 20)),
            const SizedBox(height: 10),

            // Consumer を使って ViewModel の値を UI に反映
            Consumer<CounterViewModel>(
              builder: (context, viewModel, child) {
                return Text(
                  '${viewModel.count}',
                  style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
                );
              },
            ),

            const SizedBox(height: 20),

            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // デクリメントボタン
                FloatingActionButton(
                  onPressed: () => context.read<CounterViewModel>().decrement(),
                  child: const Icon(Icons.remove),
                ),
                const SizedBox(width: 20),
                // インクリメントボタン
                FloatingActionButton(
                  onPressed: () => context.read<CounterViewModel>().increment(),
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

状態管理クラス:ChangeNotifier

状態自体を管理するのがChangeNotifierクラスです。ChangeNotifierでは状態が変化したことを通知するnotifyListenersメソッドが用意されています。ChangeNotifierは1つのクラスとして管理できるので複数のデータ型を状態に持たせることができるようになっています。

// ChangeNotifier => 状態管理を行えるリスナー付きのクラス 
// notifyListenersメソッドで変化があったことを通知する
class CounterViewModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count; // カウンターの値を取得

  void increment() {
    _count++; // カウンターを増やす
    notifyListeners(); // UIを更新
  }

  void decrement() {
    _count--; // カウンターを減らす
    notifyListeners(); // UIを更新
  }
}

ValueNotifier

Providerを導入しなくてもFlutterにはデフォルトでValueNotifierなるものが用意されています。これも状態自体を管理するクラスなのですが管理できるのは単一のデータ型のみです。

final counter = ValueNotifier<int>(0);

ValueNotifier単一のデータの状態が変化した際に自動で更新通知が発火されます。そのためcounter.value++を実行すると変更が通知され、ValueListenableBuilderで変更を観測 & UI更新を行うことができるようになっています。

// ValueListenableBuilderが変更を検知し、UIに反映
ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, _) {
    return Text(
      '${counter.value}',
      style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
    );
  },
)

ValueNotifierのサンプルコード

Providerとは?

続いて「Provider」について整理しておきます。「Provider」は値を提供するための仕組みを構築するためのものになります。少しややこしいですが概念として「Provider」が存在し、その中にProvider<T>クラスやChangeNotifierProviderなどが存在します。

具体的なクラス 概要
Provider<T> 読み取り専用のイミュータブル値
MultiProvider 複数のProviderをまとめて供給
ChangeNotifierProvider 状態と通知機能あり
FutureProvider 非同期値(Future)の提供
StreamProvider リアルタイムなStreamデータの提供

「Provider」は状態管理クラスを渡したいWidgetをラップして使用します。ラップすることで子Widgetに状態管理クラスを供給(provide)します。この供給はBuildContextを介して行われ、ラップされたWidget以下のツリー階層でBuildContextから状態管理クラスを参照できるようになります。

子Widgetから参照する

子Widgetから状態管理クラスを参照する方法はいくつかあります。

Provider.of<T>

シンプルな方法でデフォルトではlisten: trueになっており、状態変化で再ビルドされる。

final viewModel = Provider.of<CounterViewModel>(context);

context.read<T>()

一度だけ値を取得したい場合など再ビルドが必要ない時に使用。

final viewModel = context.read<CounterViewModel>();

context.watch<T>()

状態を監視し、変化するとWidgetを再ビルド。

final viewModel = context.watch<CounterViewModel>();

Consumer<T>()

一部のWidgetだけを状態変化時に再ビルド。

Consumer<CounterViewModel>(
  builder: (context, viewModel, child) {
    return Text(
      '${viewModel.count}',
      style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
    );
  },
),

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index