0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter 状態管理

Posted at

元記事

https://docs.flutter.dev/get-started/fundamentals/state-management
意訳しています.

始めに

Flutterの状態はUIや状態管理システムを表示するために使うすべてのオブジェクトを参照する.
状態管理とは,最も効率的にオブジェクト同士でアクセスするために私たちのアプリを整理し,ウィジェット間でオブジェクトを共有する方法である.

このページでは状態管理の多くの側面を見ていく.

  • StatefulWidgetを使うこと
  • コンストラクタ,InheritedWidget,コールバックを使ってウィジェット間で状態を共有すること
  • 何かが変更されたとき,他のウィジェットに知らせるためにListenableを使うこと
  • アプリケーションアーキテクチャはModel-View-ViewModelアーキテクチャになっていること

以下のビデオを見てもよいだろう.
https://youtube.com/watch?v=vU9xDLdEZtU
これはhttps://pub.dev/packages/riverpodの使い方を紹介している.

このページではサードパーティ製のパッケージは使わないが,それを使っても問題ない.

StatefulWidgetを使う

状態を行う最も単純な方法はStatefulWidgetを使うことである.
StatefulWidgetは状態を保存する.
例えば,以下のウィジェットについて考えてみる.

class MyCounter extends StatefulWidget {
  const MyCounter({super.key});

  @override
  State<MyCounter> createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        TextButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
          child: Text('Increment'),
        )
      ],
    );
  }
}

状態管理について考えるとき,このコードには2つの重要な要素がある.

  • カプセル化 : MyCounterを所有するウィジェットはcount変数にアクセスすることはできず,変更もできない
  • オブジェクトライフサイクル : MyCounterが作られたときに,_MyCounterStateオブジェクトとcount変数が作られる.それはスクリーンから取り除かれない限り存続する.これは一時的な状態の例である

詳細は以下のサイトを確認すること.
https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app
https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html?_gl=1*3d90dz*_ga*MTY0NTg1NDE5MC4xNzQwMzc3NjA0*_ga_04YGWK0175*MTc0MDU1NzYyNy41LjEuMTc0MDU2NDM5OC4wLjAuMA..

ウィジェット間で状態を共有する

以下の例のように,いくつかのシナリオではアプリは状態を保存する必要がある.

  • 状態を更新し,アプリの他のウィジェットに通知する
  • 共有された状態が更新されることを受け取り,状態が更新されたときにUIを再構築する

このセクションでは異なるウィジェット間で効率的に状態を共有する方法を示す.
最も一般的なパターンは,

  • ウィジェットコンストラクタを使う
  • InheritedWidgetを使う
  • 何かが変更されたことを親ウィジェットに知らせるコールバック

ウィジェットコンストラクタを使う

Dartオブジェクトは参照によって渡されるため,コンストラクタの中でウィジェットを使うためにオブジェクトを定義することはウィジェットにとって一般的である.
コンストラクタに渡されたどんなオブジェクトもUIを構築するために使われる.

class MyCounter extends StatelessWidget {
  final int count;
  const MyCounter({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

これにより,ウィジェットの他のユーザが,それを使用するために何を提供する必要があるかを知ることができる.

Column(
  children: [
    MyCounter(
      count: count,
    ),
    MyCounter(
      count: count,
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        setState(() {
          count++;
        });
      },
    )
  ],
)

ウィジェットコンストラクタを通じて共有されたデータにアクセスすることは,共有された依存関係をコードの中で読むことができ,大変分かりやすい.
この一般的な方法は依存性の注入といい,多くのフレームワークで使用されている.

InheritedWidgetを使う

手動でウィジェットツリーにデータを渡すと,冗長になり,不要な定型コードを引き起こす可能性がある.
FlutterはInheritedWidgetを提供する.
InheritedWidgetは親ウィジェットにデータを保存するため,子ウィジェットにデータを保存することなくデータにアクセスすることができる.

InheritedWidgetを使うために,InheritedWidgetクラスを拡張し,dependOnInheritedWidgetOfExactTypeを使って静的メソッドof()を実装する.
buildメソッドの中でof()を呼ぶウィジェットはFlutterフレームワークに管理される依存性を作ることができる.
そのため,ウィジェットが新しいデータを使って再構築されたり,updateShouldNotifyがtrueになったりしたときに,InheritedWidgetに依存するすべてのウィジェットは再構築される.

class MyState extends InheritedWidget {
  const MyState({
    super.key,
    required this.data,
    required super.child,
  });

  final String data;

  static MyState of(BuildContext context) {
    // This method looks for the nearest `MyState` widget ancestor.
    final result = context.dependOnInheritedWidgetOfExactType<MyState>();

    assert(result != null, 'No MyState found in context');

    return result!;
  }

  @override
  // This method should return true if the old widget's data is different
  // from this widget's data. If true, any widgets that depend on this widget
  // by calling `of()` will be re-built.
  bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}

次に,共有された状態にアクセスするために,ウィジェットのbuild()メソッドからof()メソッドを呼ぶ.

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    var data = MyState.of(context).data;
    return Scaffold(
      body: Center(
        child: Text(data),
      ),
    );
  }
}

コールバックを使う

コールバックを公開することによって値が変化したときに,他のウィジェットに知らせることができる.
FlutterはValueChanged型を提供する.
ValueChanged型は一つのパラメータを使って関数コールバックを宣言する.

typedef ValueChanged<T> = void Function(T value);

ウィジェットコンストラクタの中でonChangedを公開することによって,ウィジェットがonChangedを呼んだときに,このウィジェットを使用しているウィジェットが応答する方法を提供する.

class MyCounter extends StatefulWidget {
  const MyCounter({super.key, required this.onChanged});

  final ValueChanged<int> onChanged;

  @override
  State<MyCounter> createState() => _MyCounterState();
}

例えば,このウィジェットはonPressedコールバックを使って,状態が保持するcount変数の値を更新する.

TextButton(
  onPressed: () {
    widget.onChanged(count++);
  },
),

もっと深く見る

ここをチェック.
https://docs.flutter.dev/get-started/fundamentals/state-management#dive-deeper

Listenableを使う

状態を共有する方法は学んだが,状態が変化したときにどのようにUIを更新するかは学んでいない.
アプリの他の部分に通知するような方法で,共有状態を変更するにはどうすればいいのだろうか?

FlutterはListenableと呼ばれる抽象クラスを提供する.
Listenableを使ういくつかの方法は,

  • ChangeNotifierを使って,ListenableBuilderを使って定期監視する
  • ValueNotifierを使って,ValueListenableBuilderを使う

ChangeNotifier

ChangeNotifierを使うために,ChangeNotifierを拡張し,リスナーに知らせる必要があるときのみnotifyListerを呼びだす.

class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

そして,それをListenableBuilderに渡して,ChangeNotifierがリスナーを更新するたびに,ビルダー関数が返すサブツリーが再構築されるようにする.

Column(
  children: [
    ListenableBuilder(
      listenable: counterNotifier,
      builder: (context, child) {
        return Text('counter: ${counterNotifier.count}');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.increment();
      },
    ),
  ],
)

ValueNotifier

ValueNotifierは単一の変数を保存するため,ChangeNotifierの単純化したものである.
ValueListenableListenableを実装するため,ListenableBuilderValueListenableBuilderのようなウィジェットと互換性がある.
ValueNotifierを使うために,初期値を使ってValueNotifierのインスタンスを作成する.

ValueNotifier<int> counterNotifier = ValueNotifier(0);

変数を見たり更新したりするためにvalueフィールドを使い,変数が更新されたことをListenerを通じて知らせる.
ValueNotifierChangeNotifierを拡張したものであるから,Listenableであり,ListenableBuilderを使える.
builderコールバックの中で値が提供されるValueListenableBuilderも使える.

Column(
  children: [
    ValueListenableBuilder(
      valueListenable: counterNotifier,
      builder: (context, value, child) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

もっと深く見る

ここをチェック.
https://docs.flutter.dev/get-started/fundamentals/state-management#deep-dive

アプリケーションアーキテクチャであるMVVMを使う

状態を共有し,状態が変更されたときにアプリの他の部分に通知する方法を理解したので,アプリ内のステートフルオブジェクトをどのように整理するかを考え始める準備ができた.

このセクションは,Flutterのようなフレームワークがどのようなデザインパターンを実装しているのか説明する.
これはMVVMアーキテクチャと呼ぶ.

モデルの定義

モデルは通常Dartクラスで,HTTPリクエストやデータのキャッシュ,プラグインのようなシステムリソースの管理といった低レベルのタスクを行う.
モデルは通常Flutterライブラリをインポートする必要はない.

例えば,HTTPクライアントを使ってカウンタ状態を読み込んだり更新したりするモデルである.

import 'package:http/http.dart';

class CounterData {
  CounterData(this.count);

  final int count;
}

class CounterModel {
  Future<CounterData> loadCountFromServer() async {
    final uri = Uri.parse('https://myfluttercounterapp.net/count');
    final response = await get(uri);

    if (response.statusCode != 200) {
      throw ('Failed to update resource');
    }

    return CounterData(int.parse(response.body));
  }

  Future<CounterData> updateCountOnServer(int newCount) async {
    // ...
  }
}

このモデルはFlutterのコードを一切使わず,動作しているプラットフォームについても仮定しない.
唯一の仕事はHTTPクライアントを使ってカウントを取得したり更新したりすることである.
これにより,単体テストでモデルをMockやFakeで実装することができ,アプリの低レベルコンポーネントと,完全なアプリを構築するために必要な高レベルのUIコンポーネントの境界を明確に定義することができる.

CounterDataクラスはデータの構造を定義し,アプリケーションの真の「モデル」である.
通常,モデル層はアプリに必要なコア・アルゴリズムとデータ構造を担当する.
変更不可な値型を使用するなど,モデルを定義する他の方法に興味がある場合,pub.devのfreezedやbuild_collectionなどのパッケージをチェックすると良い.

ViewModelの定義

ViewModelはViewからModelに繋げる.
これは,Viewから直接アクセスModelにアクセスするの防ぎ,データフローがモデルが変化するということを遵守する.
データフローはViewModelによって扱われ,notifyListenersを通じて何かが変更したことを知らせる.
ViewModelはまるでキッチンとお客の間を取り持つレストランのウェイターのような働きを持つ.

import 'package:flutter/foundation.dart';

class CounterViewModel extends ChangeNotifier {
  final CounterModel model;
  int? count;
  String? errorMessage;
  CounterViewModel(this.model);

  Future<void> init() async {
    try {
      count = (await model.loadCountFromServer()).count;
    } catch (e) {
      errorMessage = 'Could not initialize counter';
    }
    notifyListeners();
  }

  Future<void> increment() async {
    final currentCount = count;
    if (currentCount == null) {
      throw('Not initialized');
    }
    try {
      final incrementedCount = currentCount + 1;
      await model.updateCountOnServer(incrementedCount);
      count = incrementedCount;
    } catch(e) {
      errorMessage = 'Count not update count';
    }
    notifyListeners();
  }
}

ViewModelはModelからエラーを受け取ったときのためにerroeMessageを保存する.
これはビューがランタイムエラーを扱わないことを防ぎ,クラッシュさせないようにする.
その代わり,errorMessageはViewを通じてユーザフレンドリーのエラーメッセージを表示できる.

Viewの定義

ViewModelChangeNotifierであるため,ViewModelの参照を持つすべてのウィジェットはViewModelのリスナーに通知が届いたら,Viewの再構築を行うためにListenerBuilderを使う.

ListenableBuilder(
  listenable: viewModel,
  builder: (context, child) {
    return Column(
      children: [
        if (viewModel.errorMessage != null)
          Text(
            'Error: ${viewModel.errorMessage}',
            style: Theme.of(context)
                .textTheme
                .labelSmall
                ?.apply(color: Colors.red),
          ),
        Text('Count: ${viewModel.count}'),
        TextButton(
          onPressed: () {
            viewModel.increment();
          },
          child: Text('Increment'),
        ),
      ],
    );
  },
)

このパターンは,UI設計とModelを使った低レベル処理を分けることができるようになる.

もっと状態管理について学ぼう

https://docs.flutter.dev/data-and-backend/state-mgmt/options
https://fluttersamples.com/

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?