Help us understand the problem. What is going on with this article?

Flutter state_notifierいい感じなので使ったほうが良いですよ

providerパッケージの作者であるRemi Rousseletさんが今月リリースした
flutter_state_notifierパッケージが日本のFlutter界隈でも話題になっていますね

日本で有名なFlutterユーザの方達もコミットされていますね

まだ BLoC で消耗してるの?というブログもありましたが、
change_notifierベースの書き方より気持ちいいので、今後はstate_notifierベースが増えると思います

使い方

pubspec.yaml

stateを同作者によるfreezedを使って書くと気持ちがいいです
僕が書いたfreezedの雑な解説記事はこちらにあります

ちなみに、state_notifierはFlutterに依存しないパッケージで、flutter_state_notifierがFlutterと統合されたものになっています(具体的にはProviderやLocatorMixinを挟み込む部分をやってくれている)
ので、Flutterで用いる場合は後者で

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  freezed_annotation:
  flutter_state_notifier:

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  freezed:

基本的な考え方

  • ViewにはViewのことしか書きたくない
  • ネストしたくない(StreamBuidlerやProvider.Consumer/Selectorを使いたくない)
  • 安全に状態を管理したい(Viewからは直接いじれないようにしたい)

書き方の例

HogeStateNotifierの部分の命名はViewModelだったりControllerだったり色々考えられると思うのですが、
いったん本家のexampleに合わせました
好みで変えていいと思います

命名について追記

  • hoge_page.dart
    • HogePage
      • 状態を反映したビュー
  • hoge_state.dart
    • HogeState
      • 状態を持つイミュータブルなクラス
    • HogeStateNotifier
      • Stateを操作するクラス
  • hoge_state.freezed.dart
    • 自動生成される

以下、それぞれのクラスについて見ていきます
(仕事中にいそいそ書いてるのでコードは全て疑似コードで、ツッコミは受け付けません)

State

hoge_state.dart
@freezed
abstract class HogeState with _$HogeState {
  const factory HogeState({
    @Default(0) int page,
    @Default(false) bool isEnabled,
    String content
  }) = _HogeDetailState;
}

freezedベースのイミュータブルなクラスです。
そのビューについての、変更を画面に通知したい変数を集約させているイメージです
@requiredを使うよりは@Defaultで初期値を与えるのがクールだと思います(page = 0でないのに注意)
後述しますが更新時はstate = state.copyWith(page: 1)のような感じで

StateNotifier

hoge_state.dart
class HogeStateNotifier extends StateNotifier<HogeDetailState> {
  final int hogeId;
  String draft = '';
  bool _isLoading = false;

  HogeStateNotifier({@required this.hogeId}): super(const HogeState()) {
    fetch();
  }

  void setPage(int value) {
    state = state.copyWith(page: value);
    state = state.copyWith(isEnabled: _isEnabled);
  }

  Future<void> fetch() {
    if (_isLoading) return;
    // いい感じにfetchしてるとする
    state = state.copyWith(content: content);
  }

  bool _isEnabled() {
    // stateをもとにいい感じに計算してるとする
    return true;
  }
}

stateを内部に持つ、ロジック部分を担当するクラスです
ここで注目してほしいのは、

  • 変更を画面に通知したいわけではない変数はStateNotifierが持っている
    • ここではdraft、例えば遷移時に次の画面にわたす変数だとしましょう
  • ロジックの内部でのみ使う変数もprivateでStateNotifierが持っている
    • ここでは_isLoading
  • stateの更新は全てStateNotifierで行う。Viewから弄りたいときはアクセサを生やす
    • ここではsetPage

点ですね

あと、好みだと思いますが、僕は複雑な遷移処理(例えばbottomSheetを出し、そこでの反応に応じて画面遷移する、など)もStateNotifierに書いてしまっています
Viewを複雑にしたくないという気持ちがあります

hoge_state.dart
  Future<void> pushToWebView(BuildContext context) async {
    // 遷移処理やモーダル出したりの処理
  }

あとこれも好みでしょうが、孫Viewからも参照したりしそうなHogeControllerの類もここで持っています(disposeを忘れないように)

Page

親のbuild

hoge_page.dart
  @override
  Widget build(BuildContext context) {
    return StateNotifierProvider<HogeStateNotifier, HogeState>(
      child: const _Scaffold(),
      create: (BuildContext context) => HogeStateNotifier(hogeId: widget.hogeId),
    );
  }

子での参照

以下を使い分けネストとリビルドの少ないコードを書きましょう

  • context.read<HogeStateNotifier>()
    • State更新時にリビルドされない
    • StateNotifierのメソッドを呼びたい、Stateの値を更新したい時など
  • context.read<HogeState>()
    • State更新時にリビルドされない
    • Stateの現在の値を参照したい(画面遷移で渡すなど)
  • context.select<HogeState, FugaClass>((s) => s.fuga)
    • State更新時、fugaが変化している場合のみリビルドされる
    • context.select((HogeState s) => s.fuga)でもいいよ
    • 状態の変化をViewに反映させたいとき
  • context.watch<HogeState>()
    • State更新時、常にリビルドされる
    • あまり使わないイメージ

スニペット

AndroidStudioでは以下をLive Templateに登録しておくと開発が捗りそう
命名に応じて変えて下さい

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier/state_notifier.dart';

part '$FILE_NAME$_state.freezed.dart';

@freezed
abstract class $CLASS_NAME$State with _$$$CLASS_NAME$State {
  const factory $CLASS_NAME$State({}) = _$CLASS_NAME$State;
}

class $CLASS_NAME$StateNotifier extends StateNotifier<$CLASS_NAME$State> {
  $CLASS_NAME$StateNotifier() : super(const $CLASS_NAME$State()) {}
}

結論

StateとNotifierを分離したことがChangeNotifierと比べた際の大きなメリットだと思います
notifyListener()を忘れることもなくなるし…

僕はこれから徐々に業務コードのリプレイスをやっていきます
皆さんはどうですか?

tenxia
職種別のコミュニティ・メディアを開発・運営している株式会社TenxiaのQiitaアカウントです。
https://tenxia.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした