Flutterの状態管理に使われるProviderを使ってみる!

こんにちは!

アドベントカレンダー20日目を担当しますモバイルコースのKoshiです。

今回はFlutterの常態管理で使われるProviderを紹介したいと思います。

Providerのメリット

providerを使うとViewやPageでStatelessな開発をすることができ、メモリを余分に使う必要がなくなります。また他クラスにいちいち値渡しをしなくて良いので、差分が少なくなるのでコンフリクト解消をしないといけなくなる量が減ります。

Providerの種類

主に使われるProviderには2種類あります。それぞれ見ていきましょう。

・StateNotifierProvider

・StateProvider

上の2つがそれぞれどのような時に使われるのかやそれぞれのメリットなどを以下から紹介したいと思います。

StateNotifierProvider

StateNotifierProviderは状態を管理する変数が2つ以上の時に主に使われるProviderです。状態を管理する変数をfreezedで定義して使われることが一般的です。今回はStateNotifierProviderを使った簡単な電卓アプリを作ってみましたので、これに基づいて説明していきたいと思います。

今回作成した電卓アプリのUI(GIFがどうしても作れずに画像が用意できなかったので、写真になってしまいました。ごめんなさい。)

1.モデルを作成する

まず状態を管理する変数を定義しましょう。一般的にこの変数は一緒のファイルに保管されモデルと呼ばれています。今回の電卓アプリのモデルは以下の通りです。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'calculation.freezed.dart';

@freezed
class Calculation with _$Calculation {
  const factory Calculation({
    @Default(0) int step,
    @Default(-1) int firstInputNumber,
    @Default('') String operator,
    @Default(-1) int secondInputNumber,
    @Default(-1) double resultWaru,
    @Default(-1) int resultExceptionWaru,
  }) = _Calculation;
}

今回はstep, firstInputNumber, operator, secondInputNumber, resultWaru, resultExceptionWaruの6つが状態を管理する変数となります。

このモデルを作るためにはfreezedというFlutterのパッケージを使う必要があります。

freezedパッケージをインストールしてみましょう。まず、以下のコマンドをターミナルに打って、パッケージをpubspec.yamlに追加します。

$ flutter pub add freezed_annotation
$ flutter pub add --dev build_runner
$ flutter pub add --dev freezed

次に、以下のコマンドを打って、freezedパッケージを使えるようにします。

$ flutter pub get

これでfreezedを使うことができます。

上記のモデルを作り終わったら、以下のコマンドを入力して、calculation.freezed.dartを作成します。

$ flutter pub run build_runner build

これでモデルの完成です!

2.Providerの作成

モデルが完成したら、次は状態管理を実際に行うためにStateNotifierProviderを作っていきましょう。StateNotifierProviderではモデルで定義した変数の値を更新したり、保持したりします。

StateNotifierProviderを使うためにはhooks_riverpodというパッケージを使わないといけないので、モデルを作成した時と同じようにパッケージをインストールしてください。

ではまず、今回作ったStateNotifierProviderの全体のコードを載せたいと思います。

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tech_blog_calc_app/constraiants/app_text.dart';
import 'package:tech_blog_calc_app/models/calculation.dart';

final calculationProvider =
    StateNotifierProvider<CalculationNotifier, Calculation>((ref) {
  return CalculationNotifier();
});

class CalculationNotifier extends StateNotifier<Calculation> {
  CalculationNotifier() : super(const Calculation());

  void updateFirstInputNumber(String numberText) {
    int number = int.parse(numberText);
    state = state.copyWith(firstInputNumber: number, step: state.step + 1);
  }

  void updateOperator(String operator) {
    state = state.copyWith(operator: operator, step: state.step + 1);
  }

  void updateSecondInputNumber(String numberText) {
    int number = int.parse(numberText);
    state = state.copyWith(secondInputNumber: number, step: state.step + 1);
  }

  void updateResult() {
    int resultExceptWaru = 0;
    double resultWaru = 0;
    if (state.operator == AppText.waru) {
      resultWaru = state.firstInputNumber / state.secondInputNumber;
    } else if (state.operator == AppText.x) {
      resultExceptWaru = state.firstInputNumber * state.secondInputNumber;
    } else if (state.operator == AppText.minus) {
      resultExceptWaru = state.firstInputNumber - state.secondInputNumber;
    } else if (state.operator == AppText.plus) {
      resultExceptWaru = state.firstInputNumber + state.secondInputNumber;
    }
    state = state.copyWith(
        resultWaru: resultWaru,
        resultExceptionWaru: resultExceptWaru,
        step: state.step + 1);
  }

  void resetStepData() {
    state = state.copyWith(
        step: 0,
        resultWaru: 0,
        resultExceptionWaru: 0,
        operator: '',
        firstInputNumber: 0,
        secondInputNumber: 0);
  }
}

それでは1つずつ上から解説していきたいと思います!

final calculationProvider =
    StateNotifierProvider<CalculationNotifier, Calculation>((ref) {
  return CalculationNotifier();
});

この部分でviewやpageでproviderを呼び出す時の変数を定義します。この変数はグローバル変数なので、viewやpageでこの変数を書いたファイルをインポートすれば使えるようになります。

class CalculationNotifier extends StateNotifier<Calculation> {
  CalculationNotifier() : super(const Calculation());

これはクラスの定義で、StateNotifier<Calculation>でモデルを定義したCalculationクラスをStateNotifierProviderで状態管理できるようにします。またsuper(const Calculation())で状態管理の初期値はCalculationクラスのDefaultの通りにするようにします。

void updateFirstInputNumber(String numberText) {
    int number = int.parse(numberText);
    state = state.copyWith(firstInputNumber: number, step: state.step + 1);
  }

写真の電卓の数字の部分を押したら、押された時の数字をfirstInputNumberに入れるために引数で取ってきたnumberTextをint型に変換してstate.copywithでfirstInputNumberの値をその値に更新するようにします。またstepはボタンを押すごとに1増やすためにstate.step + 1をします。

void updateOperator(String operator) {
  state = state.copyWith(operator: operator, step: state.step + 1);
}

写真の電卓の「÷」「×」「-」「+」の演算子が押されたら、押された演算子をoperatorの値に入れて更新します。

void updateSecondInputNumber(String numberText) {
  int number = int.parse(numberText);
  state = state.copyWith(secondInputNumber: number, step: state.step + 1);
}

演算子を押した後の数字はsecondInputNumberに入れるので、firstInputNumberと同じように実装します。

void updateResult() {
  int resultExceptWaru = 0;
  double resultWaru = 0;
  if (state.operator == AppText.waru) {
    resultWaru = state.firstInputNumber / state.secondInputNumber;
  } else if (state.operator == AppText.x) {
    resultExceptWaru = state.firstInputNumber * state.secondInputNumber;
  } else if (state.operator == AppText.minus) {
    resultExceptWaru = state.firstInputNumber - state.secondInputNumber;
  } else if (state.operator == AppText.plus) {
    resultExceptWaru = state.firstInputNumber + state.secondInputNumber;
  }
  state = state.copyWith(
      resultWaru: resultWaru,
      resultExceptionWaru: resultExceptWaru,
      step: state.step + 1);
}

この関数で計算をして計算結果をresultWaruとresultExceptionWaruに入れるように実装します。割り算の時は小数になる可能性があるので、double型にします。また、state.operator == AppText.waruで現在operatorに入っている値が「÷」だったらという意味を表します。

void resetStepData() {
  state = state.copyWith(
      step: 0,
      resultWaru: 0,
      resultExceptionWaru: 0,
      operator: '',
      firstInputNumber: 0,
      secondInputNumber: 0);
}

状態を管理している変数の値を全てDefaultの通りにリセットするように実装しています。

3.Providerを使ってUIを作る

StateNotifierProviderを作成したら、そのProviderを実際に使ってみましょう!

まず、StateNotifierProviderを呼び出す処理を見ていきましょう!以下がそのコードです。

class CalculationPage extends ConsumerWidget {
  const CalculationPage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final calculationNotifier = ref.watch(calculationProvider.notifier);
    final step = ref.watch(calculationProvider.select((value) => value.step));
    final operator =
        ref.watch(calculationProvider.select((value) => value.operator));

ConsumerWidgetはhooks_riverpodパッケージが必要なので、注意してください。また、ConsumerWidgetを使うときはbuild関数の引数にWidgetRef型のrefを必ず入れてください。このrefを使って今回使ったStateNotifierProviderにアクセスします。ref.watch(calculationProvider.notifier)でStateNotifierProviderで定義した関数を使えるようにします。またref.watch(calculationProvider.select((value) => value.step))で状態を管理している変数stepの値を取得することができます。watchの他にもreadやlistenがあります。詳しくは以下のリンクをご覧ください。

プロバイダの利用方法 | Riverpod
本セクションの前に「プロバイダとは」のセクションに目を通していただくことをおすすめします。

次に呼び出した変数を使って、UIに反映させてみましょう!

String _displayResult(int step, WidgetRef ref) {
  final firstInputNumber = ref
        .watch(calculationProvider.select((value) => value.firstInputNumber));
  final secondInputNumber = ref.watch(calculationProvider.select((value) => value.secondInputNumber));
  final resultWaru = ref.watch(calculationProvider.select((value) => value.resultWaru));
  final operator = ref.watch(calculationProvider.select((value) => value.operator));
  final resultExceptionWaru = ref.watch(calculationProvider.select((value) => value.resultExceptionWaru));
  if (step == 0) {
    return AppText.zero;
  } else if (step == 1) {
    return '$firstInputNumber';
  } else if (step == 2) {
    return '$firstInputNumber';
  } else if (step == 3) {
    return '$secondInputNumber';
  } else {
    return operator == AppText.waru ? '$resultWaru' : '$resultExceptionWaru';
  }
}

これは入力された値と計算結果を表示する関数です。先ほどのようにStateNotifierProviderを呼び出し、その値を使って、if文で条件分岐しています。返す値を状態管理している値にすることで、状態管理した値がUIに反映されるようになります。このように状態管理をしてあげることで、StatelessなUI設計をすることができます。

StateProvider

StateProviderは状態を管理する変数が1つしかない時に主に使われます。StateNotifierProviderほど複雑ではないので、変数が1つしかない時は基本的にStateProviderを使った方が楽です!

では使い方を見ていきましょう!

StateProviderもhooks_riverpodパッケージが必要なので注意してください。

StateProviderはモデルを定義しなくても使うことができます。StateNotifierProviderに比べて非常に楽です!

final counterProvider = StateProvider<int>((ref) => 0);

これで1つの状態を管理できるStateProviderが作れます!これもStateNotifierProvider同様グローバル変数なので、これが書いてあるファイルをインポートすればviewやpageで使えます。StateProvider<int>((ref) => 0)で状態管理する型はintで初期値は0であることを意味しています。

Widget build(BuildContext context, WidgetRef ref) {
   //StateProviderで状態管理している値を取得する。
    final count = ref.watch(counterProvider);
    return Scaffold(
    	appBar: AppBar(
        	title: Text('StateProvider'),
        ),
        body: Center(
        	//ここで状態管理している値を表示する。
        	child: Text('$count'),
        ),
        floatingActionButton: FloatingActionButton(
        	onPressed: () {
            	//StateProviderを呼び出す
                final counterNotifier = ref.watch(counterProvider.notifier);
                //状態管理している値を更新する
                counterNotifier.state = count + 1;
            }
        )
    );
}

上のコードは現在、状態管理されている値をUIに表示し、ボタンを押したら、1だけカウントが増える処理を実装したものです。

状態管理している値を更新する方法は他にも以下のような書き方があります。

counterNotifier.update((state) => state + 1);

この書き方は今回みたいに直近のstateを使う場合に書きます。

終わりに

長かったですが、ここまで読んでくださりありがとうございました。

案件に入ってProviderを本格的に使ったので、記事にしてみました。

アドベントカレンダー明日はHiroyoshi Takahashiさんです!お楽しみに〜