まだ BLoC で消耗してるの?

タイトルはすいません🙇 Flutter の状態管理には BLoC (Business Logic Component) パターンがよく使われると思うんですが、package:provider (正確には provider と ChangeNotifier) を使った方が楽だよ、という記事です。 Google I/O でも 2018 年は BLoC を推奨していましたが (Build reactive mobile apps with Flutter (Google I/O '18))、 2019 年では意見を変えて provider パッケージの使用を推奨しています (Pragmatic State Management in Flutter (Google I/O'19) )。ちなみに 2019 の発表は現地で見ていたのですが、終盤にもかかわらず満席で Flutter への注目度の高さを伺わせました。

目次

BLoC はどうしてよくないのか

BLoC それ自身が悪いわけではないです。私も一度 BLoC パターンでアプリを構築したことはあり、StreamController や StreamBuilder (Stream は Rx でいう Observable に相当) などを使っていい具合にコードがかけた時は、いい気分ですし、何の支障もなかったです。しかし、本当にこのコード量や複雑性が必要なのだろうかとはよく思っていました(BLoC ではStreamとSink を入出力で使うことが要求されている)。 つまり BLoC は、複雑で学習コストが高いところに問題があります。 そこで、より簡易に状態管理を行う手法として、 package:provider (と ChangeNotifier)が推奨されました。

package:provider とは

package:providerは、コミュニティによって開発されている DI (Dependency Injection) 及び 状態管理用のパッケージです。 実は Google 側でも似たようなパッケージ(provide)を開発していたのですが、provider の方がよいのでは?となり、今は provider を推奨しているらしいです。

package:provider は効率的な状態へのアクセス制御や変更通知の機構 を提供します。 基本的には、上位 Widget でProviderによって状態を作成し、下位 Widget で ConsumerSelector を使って状態にアクセスします。 実際のコードを見た方がわかりやすいと思いますので、次節をご覧ください。

サンプルコード

package:provider を使った状態管理の方法について、順を追って説明していきます。例のカウントアップするだけのアプリを簡略化したものを基にします。

一般的な StatefulWidget によるパターン

さて、まずこちらが、一般的な StatefulWidget による状態管理です。setStateを呼ぶことによって、再描画が行われます。状態やロジックが、UI にくっついていて、他の Widget からの状態操作やロジックのテストがしにくいです。provider を使った場合、どうなるでしょうか?(provider を使用したパターン)





















 
 
 













import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(home: MyHomePage());
  }
}

class MyHomePage extends StatefulWidget {
  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _count = 0;

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('$_count')),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
      ),
    );
  }
}

package:provider を使用したパターン

続いて、package:provider を使ったパターンを見ていきます。

状態クラスを作る

まず、状態クラスを作ります。ChangeNotifier を mixin することで notifyListeners が使えるようになります。この関数が呼ばれると、変更を監視している Widget に状態の変更が通知されます。






 



class CounterStore with ChangeNotifier {
  var count = 0;

  void incrementCounter() {
    count++;
    notifyListeners();
  }
}

状態クラスをアクセスできるようにする

下位 Widget が状態クラスにアクセスできるように ChangeNotifierProviderを Widget ツリーの上の方に配置します(スコープを制限できる)。複数の状態クラスには MultiProvider が使えます。





 
 






class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: MyHomePage(),
      ),
    );
  }
}

状態クラスにアクセスする

最後に、状態クラスにアクセスします。それには主に ConsumerSelector を通じて行います。Selector は監視対象を変数一つに絞ることができますが、ここでは通知される変数は count ひとつですから、 Consumerを使います。

ポイント

  • 状態管理を分離できたので StatelessWidget を使える
  • ロジックが分離されている



 


 

 







class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Consumer<CounterStore>(
      builder: (context, counterStore, _) {
        return Scaffold(
          body: Center(child: Text('${counterStore.count}')),
          floatingActionButton: FloatingActionButton(
            onPressed: counterStore.incrementCounter,
          ),
        );
      },
    );
  }
}

コード全体

コード全体を貼っておきます。直感的で必要最小限のコードになっていると思います。 実際のプロジェクトでは、ファイルを分離しましょう。 ロジック、状態を分離できたので、テストを簡単に行うことができます。

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

class CounterStore with ChangeNotifier {
  var count = 0;

  void incrementCounter() {
    count++;
    notifyListeners();
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Consumer<CounterStore>(
      builder: (context, counterStore, _) {
        return Scaffold(
          body: Center(child: Text('${counterStore.count}')),
          floatingActionButton: FloatingActionButton(
            onPressed: counterStore.incrementCounter,
          ),
        );
      },
    );
  }
}

さて、package:providerChangeNotifier を使ったパターンはいかがだったでしょうか? 学習することが少ないため、初心者にもおすすめだと思います。BLoC を使っている人も是非一度使ってみてください😄

参考資料


Share:
管理人:
follow @ytiskw
Last Updated: 3/12/2020, 2:38:02 PM