システム開発部のTです。
以前、Fluxで組んでいるアプリがあり、コードも分かりやすい感じでまとめられていたのを思い出しました。
そこで、FlutterでもFluxで組んでみたいと思い立って、今回挑戦してみました。
「Flutter Flux」で検索かけてみると、結構検索結果として表示されているのですが、その中で以下のものが気になりました。

https://github.com/google/flutter_flux

GoogleがFlutter用のFluxパターンを作っていた????
非常に興味深い!
ということで、本件では上記のFluxパターンを元にして実装してみましたので、
そのレポートを書いていきたいと思います。

実装イメージ

こんな感じの動きになります。

開発準備

プロジェクトの生成

以下のコマンドを実行し、プロジェクトを生成してください。
(以下、helloworldにしていますが、任意のプロジェクト名にしてください。)

$ flutter create helloworld

(※Flutterの開発環境は構築済みの前提で進めていきます。)

Fluxのテンプレートクラスの生成

githubから持ってくればいいんじゃないの???
と思った方もいるかと思いますが、そもそも最終更新日が3年前くらいなので、
null safetyにも対応しておらず、そのままでは利用できないので、加工していきます。

以下、加工後のファイルの中身になります。

action.dart
// Copyright 2015 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:async';

typedef void OnData<T>(T event);

/// A command that can be dispatched and listened to.
///
/// An [Action] manages a collection of listeners and the manner of
/// their invocation. It *does not* rely on [Stream] for managing listeners. By
/// managing its own listeners, an [Action] can track a [Future] that completes
/// when all registered listeners have completed. This allows consumers to use
/// `await` to wait for an action to finish processing.
///
///     var asyncListenerCompleted = false;
///     action.listen((_) async {
///       await new Future.delayed(new Duration(milliseconds: 100), () {
///         asyncListenerCompleted = true;
///       });
///     });
///
///     var future = action();
///     print(asyncListenerCompleted); // => 'false'
///
///     await future;
///     print(asyncListenerCompleted). // => 'true'
///
/// Providing a [Future] for listener completion makes actions far easier to use
/// when a consumer needs to check state changes immediately after invoking an
/// action.
///
class FluxAction<T> implements Function {
  List<OnData<T>> _listeners = <OnData<T>>[];

  /// Dispatch this [FluxAction] to all listeners. If a payload is supplied, it will
  /// be passed to each listener's callback, otherwise null will be passed.
  Future<List<dynamic>> call({required T payload}) {
    // Invoke all listeners in a microtask to enable waiting on futures. The
    // microtask queue is emptied before the event loop continues. This ensures
    // synchronous listeners are invoked in the current tick of the event loop
    // without being scheduled at the back of the event queue. A Dart [Stream]
    // behaves in a similar fashion.
    //
    // Performance benchmarks over 10,000 samples show no performance
    // degradation when dispatching actions using this action implementation vs
    // a [Stream]-based action implementation. At smaller sample sizes this
    // implementation slows down in comparison, yielding average times of 0.1 ms
    // for stream-based actions vs. 0.14 ms for this action implementation.
    return Future.wait<dynamic>(
      _listeners.map(
        (OnData<T> l) => new Future<dynamic>.microtask(() => l(payload))
      ),
    );
  }

  /// Cancel all subscriptions that exist on this [FluxAction] as a result of
  /// [listen] being called. Useful when tearing down a flux cycle in some
  /// module or unit test.
  void clearListeners() => _listeners.clear();

  /// Supply a callback that will be called any time this [FluxAction] is
  /// dispatched. A payload of type [T] will be passed to the callback if
  /// supplied at dispatch time, otherwise null will be passed. Returns an
  /// [ActionSubscription] which provides means to cancel the subscription.
  ActionSubscription listen(OnData<T> onData) {
    _listeners.add(onData);
    return new ActionSubscription(() => _listeners.remove(onData));
  }
}

typedef void _OnCancel();

/// A subscription used to cancel registered listeners to an [FluxAction].
class ActionSubscription {
  final _OnCancel _onCancel;

  ActionSubscription(this._onCancel);

  /// Cancel this subscription to an [FluxAction]
  void cancel() {
    if (_onCancel != null) {
      _onCancel();
    }
  }
}
store.dart
// Copyright 2015 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:async';
import 'action.dart';

/// A `Store` is a repository and manager of app state. This class should be
/// extended to fit the needs of your application and its data. The number and
/// hierarchy of stores is dependent upon the state management needs of your
/// application.
///
/// General guidelines with respect to a `Store`'s data:
/// - A `Store`'s data should not be exposed for direct mutation.
/// - A `Store`'s data should be mutated internally in response to [Action]s.
/// - A `Store` should expose relevant data ONLY via public getters.
///
/// To receive notifications of a `Store`'s data mutations, `Store`s can be
/// listened to. Whenever a `Store`'s data is mutated, the `trigger` method is
/// used to tell all registered listeners that updated data is available.
///
/// In a typical application using `w_flux`, a [FluxComponent] listens to
/// `Store`s, triggering re-rendering of the UI elements based on the updated
/// `Store` data.
class FluxStore {
  /// Construct a new [FluxStore] instance.
  FluxStore() {
    _streamController = new StreamController<FluxStore>();
    _stream = _streamController.stream.asBroadcastStream();
  }

  /// Construct a new [FluxStore] instance with a transformer.
  ///
  /// The standard behavior of the "trigger" stream will be modified. The
  /// underlying stream will be transformed using [transformer].
  ///
  /// As an example, [transformer] could be used to throttle the number of
  /// triggers this [FluxStore] emits for state that may update extremely frequently
  /// (like scroll position).
  FluxStore.withTransformer(StreamTransformer<FluxStore, dynamic> transformer) {
    _streamController = new StreamController<FluxStore>();

    // apply a transform to the stream if supplied
    _stream =
        _streamController.stream.transform<dynamic>(transformer).asBroadcastStream() as Stream<FluxStore>;
  }

  /// Stream controller for [_stream]. Used by [trigger].
  late StreamController<FluxStore> _streamController;

  /// Broadcast stream of "data updated" events. Listened to in [listen].
  late Stream<FluxStore> _stream;

  void dispose() {
    _streamController.close();
  }

  /// Trigger a "data updated" event. All registered listeners of this `Store`
  /// will receive the event, at which point they can use the latest data
  /// from this `Store` as necessary.
  ///
  /// This should be called whenever this `Store`'s data has finished mutating in
  /// response to an action.
  void trigger() {
    _streamController.add(this);
  }

  /// A convenience method for listening to an [action] and triggering
  /// automatically. The callback doesn't call return, so the return
  /// type of onAction is null.
  void triggerOnAction<T>(FluxAction<T> action,
      {dynamic onAction(T payload)?}) {
    if (onAction != null) {
      action.listen((T payload) async {
        await onAction(payload);
        trigger();
      });
    } else {
      action.listen((dynamic _) {
        trigger();
      });
    }
  }

  /// A convenience method for listening to an [action] and triggering
  /// automatically once the callback returns true when it completes.
  ///
  /// [onAction] will be called every time [action] is dispatched.
  /// If [onAction] returns a [Future], [trigger] will not be
  /// called until that future has resolved and the function returns either
  /// void (null) or true.
  void triggerOnConditionalAction<T>(
      FluxAction<T> action, FutureOr<bool> onAction(T payload)) {
    action.listen((dynamic payload) async {
      // Action functions must return bool, or a Future<bool>.
      dynamic result = onAction(payload);
      bool wasChanged;
      if (result is Future) {
        wasChanged = await result;
      } else {
        wasChanged = result;
      }
      if (wasChanged) {
        trigger();
      }
    });
  }

  /// Adds a subscription to this `Store`.
  ///
  /// Each time this `Store` triggers (by calling [trigger]), indicating that
  /// data has been mutated, [onData] will be called.
  StreamSubscription<FluxStore> listen(void onData(FluxStore event),
      {Function? onError, void onDone()?, bool? cancelOnError}) {
    return _stream.listen(onData,
        onError: onError, onDone: onDone, cancelOnError: cancelOnError);
  }
}
store_watcher.dart
// Copyright 2016 The Chromium Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'store.dart';

/// Signature for a function the lets the caller listen to a store.
typedef FluxStore ListenToStore(StoreToken token, {ValueChanged<FluxStore>? onStoreChanged});

/// A widget that rebuilds when the [FluxStore]s it is listening to change.
abstract class StoreWatcher extends StatefulWidget {
  /// Creates a widget that watches stores.
  StoreWatcher({ required Key key }) : super(key: key);

  /// Override this function to build widgets that depend on the current value
  /// of the store.
  @protected
  Widget build(BuildContext context, Map<StoreToken, FluxStore> stores);

  /// Override this function to configure which stores to listen to.
  ///
  /// This function is called by [StoreWatcherState] during its
  /// [State.initState] lifecycle callback, which means it is called once per
  /// inflation of the widget. As a result, the set of stores you listen to
  /// should not depend on any constructor parameters for this object because
  /// if the parent rebuilds and supplies new constructor arguments, this
  /// function will not be called again.
  @protected
  void initStores(ListenToStore listenToStore);

  @override
  StoreWatcherState createState() => new StoreWatcherState();
}

/// State for a [StoreWatcher] widget.
class StoreWatcherState extends State<StoreWatcher> with StoreWatcherMixin<StoreWatcher> {

  final Map<StoreToken, FluxStore> _storeTokens = <StoreToken, FluxStore>{};

  @override
  void initState() {
    widget.initStores(listenToStore);
    super.initState();
  }

  /// Start receiving notifications from the given store, optionally routed
  /// to the given function.
  ///
  /// The default action is to call setState(). In general, you want to use the
  /// default function, which rebuilds everything, and let the framework figure
  /// out the delta of what changed.
  @override
  FluxStore listenToStore(StoreToken token, {ValueChanged<FluxStore>? onStoreChanged}) {
    final FluxStore store = super.listenToStore(token, onStoreChanged: onStoreChanged);
    _storeTokens[token] = store;
    return store;
  }

  @override
  Widget build(BuildContext context) {
    return widget.build(context, _storeTokens);
  }
}

/// Listens to changes in a number of different stores.
///
/// Used by [StoreWatcher] to track which stores the widget is listening to.
mixin StoreWatcherMixin<T extends StatefulWidget> on State<T>{
  final Map<FluxStore, StreamSubscription<FluxStore>> _streamSubscriptions = <FluxStore, StreamSubscription<FluxStore>>{};

  /// Start receiving notifications from the given store, optionally routed
  /// to the given function.
  ///
  /// By default, [onStoreChanged] will be called when the store changes.
  @protected
  FluxStore listenToStore(StoreToken token, {ValueChanged<FluxStore>? onStoreChanged}) {
    final FluxStore store = token._value;
    _streamSubscriptions[store] = store.listen(onStoreChanged ?? _handleStoreChanged);
    return store;
  }

  /// Stop receiving notifications from the given store.
  @protected
  void unlistenFromStore(FluxStore store) {
    _streamSubscriptions[store]?.cancel();
    _streamSubscriptions.remove(store);
  }

  /// Cancel all store subscriptions.
  @override
  void dispose() {
    final Iterable<StreamSubscription<FluxStore>> subscriptions =
      _streamSubscriptions.values;
    for (final StreamSubscription<FluxStore> subscription in subscriptions)
      subscription.cancel();
    _streamSubscriptions.clear();
    super.dispose();
  }

  void _handleStoreChanged(FluxStore store) {
    // TODO(abarth): We cancel our subscriptions in [dispose], which means we
    // shouldn't receive this callback when we're not mounted. If that's the
    // case, we should change this check into an assert that we are mounted.
    if (!mounted)
      return;
    setState(() { });
  }
}

/// Represent a store so it can be returned by [StoreListener.listenToStore].
///
/// Used to make sure that callers never reference the store without calling
/// listen() first. In the example below, _itemStore would not be globally
/// available:
///
/// ```dart
/// final _itemStore = new AppStore(actions);
/// final itemStoreToken = new StoreToken(_itemStore);
/// ```
class StoreToken {
  /// Creates a store token for the given store.
  StoreToken(this._value);

  final FluxStore _value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final StoreToken typedOther = other;
    return identical(_value, typedOther._value);
  }

  @override
  int get hashCode => identityHashCode(_value);

  @override
  String toString() => '[${_value.runtimeType}(${_value.hashCode})]';
}
flutter_flux.dart
// Copyright 2015 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// The flutter_flux library implements a unidirectional data flow pattern
/// comprised of [Action]s, [Store]s, and [StoreWatcher]s.
///
/// - [Action]s initiate mutation of app data that resides in [Store]s.
/// - Data mutations within [Store]s trigger re-rendering of a widget (defined
///   in [StoreWatcher]s).
/// - [StoreWatcher]s dispatch [Action]s in response to user interaction.

export 'src/action.dart';
export 'src/store.dart';
export 'src/store_watcher.dart';

上記で加工したファイルを、以下のように配置しました。

※上記で利用しているaction.dart、store.dartのクラス名はオリジナルのクラス名と意図的に変更しております。
Action→FluxAction、Store→FluxStore
flutter/material.dartにも同じ名前のActionクラスが存在しており、ビルドでエラーになってしまうため。

main.dartの整理

プロジェクト作成直後に生成されているmain.dartを見てみましょう。
(以下、不要なコメントは削除済みです)

main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

このままだと見栄えが悪いので、classごとにファイルを分けます。
分けたあとの内容が以下になります。

main.dart
import 'package:flutter/material.dart';
import 'package:helloworld/MyApp.dart';

void main() {
  runApp(MyApp());
}
MyApp.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:helloworld/MyHomePage.dart';

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
MyHomePage.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

動作確認

上記までコード分けしたら、動作確認してみます。
以下の画面が表示されることを確認してください。

これで開発準備は完了です。

Fluxパターンに置き換え

毎度おなじみの「+」ボタン押下で画面中央の値が変わるというサンプルコードの仕様をそのままFluxに置き換えてみます。

Actionの定義

まずは以下の内容で、Actionの定義を作ります。

import 'package:flux_test_app/flux/src/action.dart';

final FluxAction<int> incrementAction = FluxAction<int>();

本件では、インクリメントのアクションのみなので、上記の定義一つだけになります。
intにはAction時に渡すパラメータを設定します。
本件では、カウンターの既存値を渡してインクリメントしたいので、intにしています。

Storeの定義

Storeの定義を作成します。

import 'IncrementAction.dart';
import 'flux/src/store.dart';
import 'flux/src/store_watcher.dart';

// IncrementStoreのオブジェクト取得時に利用する
final StoreToken incrementStoreToken = StoreToken(IncrementStore());

class IncrementStore extends FluxStore {

  int _counter = 0;

  IncrementStore() {
    // コンストラクタで、Action実行時のトリガー処理を定義
    triggerOnAction(incrementAction, onAction: (int value){
      // Actionからの引数に+1したものを_counterに設定
      _counter = value + 1;
    });
  }

  int get counter => _counter;
}

MyHomePageのコード変更

_MyHomePageStateクラスの以下の箇所を変更します。

class _MyHomePageState extends State<MyHomePage>  with StoreWatcherMixin<MyHomePage> { // with StoreWatcherMixin追加
  // int _counter = 0;  <--- Store側でカウンター管理するので、これを削除し
  late IncrementStore incrementStore;   // <--- 代わりにStoreを追加

initState()をオーバーライドし、listenToStoreでIncrementStoreを取得

  @override
  void initState() {
    super.initState();
    incrementStore = listenToStore(incrementStoreToken, onStoreChanged: null) as IncrementStore;
  }

_incrementCounter()メソッドのsetStateを削除し、incrementAction()を実行する。

  void _incrementCounter() {
    incrementAction(payload: incrementStore.counter);
  }

以上でMyHomePageの置き換え完了です。
置き換え後のコードは以下になります。

置き換え後のMyHomePage.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

import 'IncrementAction.dart';
import 'IncrementStore.dart';
import 'flux/src/store_watcher.dart';

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with StoreWatcherMixin<MyHomePage> {
  late IncrementStore incrementStore;

  @override
  void initState() {
    super.initState();
    incrementStore = listenToStore(incrementStoreToken, onStoreChanged: null) as IncrementStore;
  }

  void _incrementCounter() {
    incrementAction(payload: incrementStore.counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${incrementStore.counter}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

動作確認

ここまで実装完了したら、アプリを起動してください。
変更前と同様に動作することができればOKです。

Actionの追加方法

さすがにActionが一つだけというと物足りないところではあるので、本件のパターンでActionを追加するときのコーディングについて記載します。

IncrementAction.dartに追記

Incrementが加算なので、減算Decrementの追加をしてみます。
IncrementAction.dartに以下の内容で追記します。
(※「IncrementAction.dart」なのに、decrementAction追加は違和感ありますが・・・)

import 'package:flux_test_app/flux/src/action.dart';

final FluxAction<int> incrementAction = FluxAction<int>();
final FluxAction<int> decrementAction = FluxAction<int>();

IncrementStore.dartにアクショントリガー追記

上記で追加したアクション実行時のトリガーを追加します。

import 'IncrementAction.dart';
import 'flux/src/store.dart';
import 'flux/src/store_watcher.dart';


final StoreToken incrementStoreToken = StoreToken(IncrementStore());

class IncrementStore extends FluxStore {

  int _counter = 0;

  IncrementStore() {
    triggerOnAction(incrementAction, onAction: (int value){
      _counter = value + 1;
    });
    // マイナス時のアクショントリガー追加
    triggerOnAction(decrementAction, onAction: (int value){
      _counter = value - 1;
      if (_counter < 0) {
        _counter = 0;
      }
    });
  }

  int get counter => _counter;
}

MyHomePage.dartの編集

_decrementCounter()を追加し、そこにアクションの実行を設定します。

  void _incrementCounter() {
    incrementAction(payload: incrementStore.counter);
  }

  // 以下の内容を追加
  void _decrementCounter() {
    decrementAction(payload: incrementStore.counter);
  }

デクリメント用のボタンを追加します。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${incrementStore.counter}',
              style: Theme.of(context).textTheme.headline4,
            ),
            FloatingActionButton(    //  <--- ここに追加
              onPressed: _decrementCounter,   // <--- デクリメントするメソッドを設定
              tooltip: 'Decrement',
              child: Icon(Icons.exposure_minus_1),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

ここまで追加したら、アプリを実行します。
「-1」ボタンを押下したら、カウンターがデクリメントされることが確認できるかと思います。

このように、Fluxパターンでは、Actionの追加も容易にできることが、ご理解いただけたかと思います。

まとめ

Googleがgithub上でFluxパターンを公開しており、せっかくなのでそのパターンを実装してみました。
確かにFluxな動きかな・・・と思っておりますが、なんせ3年前のコードになるので、最新のFlutterの機能であれば、もっと効率的なコードで実装できるのでは?と率直に思いました。

業務で利用できるかは何ともいえないところですが、データの流れが一方向になるのは魅力的かと思います。
そういう意味ではReduxという選択肢もあるので、引き続き研究していければと思います。

なお、開発者T的にはMVVMがシンプルでいいかな?と思っています。

以上、ありがとうございました。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる