223
183

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-16

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()を忘れることもなくなるし…

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

223
183
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
223
183

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?