SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Flutterで始めるモバイルアプリ開発

【Flutter解説】データをもとに画面処理を行う方法──ビジネスロジックと画面処理のアーキテクチャを知ろう

Flutterで始めるモバイルアプリ開発 第24回

変更(イベント)通知(ProviderとChangeNotifier/Consumer)を用いる方法

 画面更新をするプログラムにおいて、変更通知を用いる実装方法は他にも多々あり、非常に分かりやすいのが特徴です。Flutterにおいて広く使われている変更通知を管理する方法の1つがProviderパッケージを使う方法です。

インストール方法

 Providerは標準パッケージではないため、リスト2のように追加インストールが必要です。

[リスト2]Providerパッケージのインストール方法
$flutter pub add provider
-- pubspec.yamlへ記述する場合 --
dependencies:
  provider: ^6.0.5

全体の構造

 まず、実際のコードを説明する前に全体の構造について示したのが図3です。ここでの大きな特徴は、画面処理とビジネスロジックが完全に分離していることです。また、細かい名称部分はこの後紹介するコード例と照らし合わせてください。

図3:変更(イベント)通知を用いた場合の構造
図3:変更(イベント)通知を用いた場合の構造

コード例

 構造に対応した実装コード例がリスト3になります。

[リスト3]Providerを使った場合の実装例(lib/ProviderScreen.dartの抜粋)
// (1) イベント通知を管理するオブジェクト
class WeekModel with ChangeNotifier {
  bool _loading = false;
  List<DateItem> _items = [];

  // (2) 外部から参照できる変数
  List<DateItem> get items => _items;
  bool get loading => _loading;

  // (3) ロード処理
  void load() async{
    _loading = true;
    // (4) ローディング開始
    Future.delayed(Duration.zero,(){
      notifyListeners();
    });

    var res = await http.get(Uri.parse("http://192.168.1.1/list.json"),headers: {
      'Accept' : 'application/json'
    });
    var json = jsonDecode(utf8.decode(res.bodyBytes));
    _items = json['items'].map<DateItem>((e) => DateItem.fromJson(e)).toList();
    _loading = false;
    // (5) 処理完了通知
    notifyListeners();
  }
}
class ProviderScreen extends StatelessWidget{
  const ProviderScreen({super.key});
  @override
  Widget build(BuildContext context) {
    // (6) イベント管理する親ウィジェット
    return ChangeNotifierProvider(
        create: (_) => WeekModel(),
        child: Scaffold(
          appBar: AppBar(
              title: const Text('サンプル(2)'),
              actions: [
                Builder(
                    builder: (context){
                      // (7) リロード処理
                      final week = Provider.of<WeekModel>(context, listen: true);
                      return IconButton(
                          onPressed: week.loading ? null : (){
                            week.load();
                          },
                          icon: const Icon(Icons.refresh));
                    })
            ],
          ),
          body: const DateListScreen(),
        ),
    );
  }
}

class _DateListScreen extends State<DateListScreen>{
  @override
  void initState() {
    // (8) ロード処理
    final week = Provider.of<WeekModel>(context, listen: false);
    week.load();
  }
  @override
  Widget build(BuildContext context) {
    // (9) イベントを受け取るウィジェット
    return Consumer<WeekModel>(
        builder: (context,week,child) {
          return Stack(
            children: [
              Column(
                children: _createListChildren(context, week),
              ),
              if(week.loading)
                _loadingWidget(context)
            ],
          );
        }
    );
  }
}

 まず、最初にビジネスロジックをクラスを定義したのが(1)です。ProviderではChangeNotifierをmixinsしたクラスを作成する必要があります。そして、変数はState管理した同様の変数を(2)のように用意します。

 そして実際にJsonデータを取得する処理が(3)です。このメソッドを他のプログラムからコールします。

 他のプログラムに対してこのインスタンスが変更されたことを通知する処理が(4)(5)であり、その場合にはnotifyListeners()をコールすると該当する画面更新が実行されます。

 ただし、(4)ではFuture.delayed()を使って通知を遅らせています。これは、initState()処理内で実行された場合のエラー回避方法の一例です。

 この処理を入れないと、画面の表示処理が終わっていない間にnotifyListeners()が実行され、その結果、再描画しようとしてエラーが生じます。そのため、このように画面描写の次のFrameに処理をまわすようにしています。

 この問題は、次に紹介するBLoCパターンでは考慮する必要がないような構造になっているので、実装の違いによる制限を知る上でも参考になるはずです。

 ここまでが、データの変更通知をするためのクラスです。今回は同一コード内に記述しましたが、実際にはコードを分割することで画面処理とロジックを分離して管理することができます。

 変更通知を受け取るウィジェットが、(6)のChangeNotifierProviderです。このウィジェットのcreateで先ほど作成したWeekModelのインスタンスを指定します。

 このインスタンスにアクセスできるのは、このウィジェット配下のみになります。画面初期化やリロード処理などでインスタンスにアクセスしているのが(7)(8)です。listenプロパティがありますが、画面の変更通知が必要な場合にはtrueを指定し、必要ない場合にはfalseを指定します。(7)では処理中はボタンがdisabled状態になるようにしているのでtrueを指定する必要があり、initState()処理内では画面表示は行えないので、falseを指定することになります。

 また、(7)のようにしてWeekModelインスタンスにアクセスする方法もありますが、(9)のようにConsumerウィジェットを使うことで変更通知が来た場合には自動的に画面を再描写することもできます。

 そして、今回はinitState()で初期データをロードするためにStatefulWidgetとして定義が必要でしたが、Providerを使えば値の変更管理をStateで行う必要がないためStatelessWidgetとして定義することも可能になります。

次のページ
BLoCパターン(ProviderとStreamController/StreamBuilder)を用いる方法

この記事は参考になりましたか?

Flutterで始めるモバイルアプリ開発連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 小林 昌弘(コバヤシ マサヒロ)

WINGSプロジェクト について> 有限会社 WINGSプロジェクト が運営する、テクニカル執筆コミュニティ(代表 ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/18105 2023/08/21 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー