Provider Patternを使い、FlutterアプリUIとロジックを分離するための考察

2021-05-26

Provider Patternを使ってFlutterアプリのUIとロジックを分離する方法

大規模アプリを開発する場合、いきあたりばったりで作り込んでいくと機能を追加することに苦痛を伴います。 そこでGoogle社は、BloC Patternを使うことが2018年ぐらいに推奨していました。

ただし、個人で作るアプリにBLoC Patternはやりすぎ(らしい)。 かと言って何も何もないとUIとロジックがぐちゃぐちゃになります。

調べてみるとProviderを使うやり方がオススメらしいので見ていきましょう。

実装

1時間ぐらいネットの記事読んで理解したつもりになり書き始めたので正しいのか疑問ですが…書いてみましょう。

急ぐ人向け

とりあえず動かしてみたい人向けにgithubレポジトリに置いときました。 適当にやってください。

git clone https://github.com/kenpos/providerPatternSample.git
flutter pub get
flutter run

FlutterにProviderを追加する

適当にプロジェクトを作成し、プロジェクトにproviderを追加する。 サンプルプロジェクトをベースに作り変えていきます。

flutter pub add provider

フォルダ構成

modelsにはボタンを押されたときの処理などロジックを固めておきます。 viewにはUIを固めておきます。

lib
│ main.dart
│
├─models
│      logic.dart
│
└─view
        ui.dart

main.dart

main.dartでは、MyAppを読み出すだけです。

import 'package:flutter/material.dart';
import 'package:mvvm_hotpepper/view/ui.dart';

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

models/logic.dart

providerのChangeNotifierを継承したクラスを作ります。 各関数終わりのnotifyListeners()の通知をトリガーにUIの更新をかけています。 increment()はボタンが押された時に呼ばれ、内部変数をインクリメントしています。 changeText()はおまけです。

import 'package:flutter/material.dart';

class CountModel extends ChangeNotifier {
  int count = 0;
  String address = "ウマ娘";

  void increment() {
    count++;
    changeText();
    notifyListeners();
  }

  void changeText() {
    if (count > 5) {
      address = "ウマ娘プリティーダービー";
    }
  }
}

view/ui.dart

これから作るアプリを部品ごとに分割していきます。 class毎に場所を示すとこのような作りになっています。

画面

ChangeNotifierProviderの呼び出し

MyAppから呼び出す中身のボディ部分を作ります。 ChangeNotifierProviderを返しています。 CountModel()というのは自前で作り込むロジッククラスです。

class TopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CountModel>(
      create: (context) => CountModel(),
      child: AppBody(),
    );
  }
}

黄色枠の部分の作る

childで呼び出しているAppBodyを作ります。 右下にあるfloatingActionButtonを押されると CountModel()クラスの中の関数increment()を呼び出しています。

class AppBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CountModel>(
        builder: (context, model, child) => Scaffold(
            appBar: AppBar(
              title: Text('Flutter Demo Home Page'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  PostCode(),
                  CountText(),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: model.increment,
              tooltip: 'Increment',
              child: Icon(Icons.search),
            )));
  }
}

赤枠部分の文字列を作り込み

PostCodeとかaddressとかは、簡単なREST APIを試そうと思いこの名前になっています。 簡単なAPIっていうのは郵便番号から住所を取得するよくあるやつです。

今は適当な文字列を入れてます。

class PostCode extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// context からModelの値が使える
      '${Provider.of<CountModel>(context).address}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

青枠部分の数字がインクリメントされる部分を書く

Textボックスに文字列を返すようになっています。 この数値の元はCountModel()の変数countです。

'${Provider.of<CountModel>(context).count}'
class CountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      '${Provider.of<CountModel>(context).count}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

全体コード

import 'package:mvvm_hotpepper/models/logic.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: TopPage(),
    );
  }
}

class TopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CountModel>(
      create: (context) => CountModel(),
      child: AppBody(),
    );
  }
}

class AppBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CountModel>(
        builder: (context, model, child) => Scaffold(
            appBar: AppBar(
              title: Text('Flutter Demo Home Page'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  PostCode(),
                  CountText(),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: model.increment,
              tooltip: 'Increment',
              child: Icon(Icons.search),
            )));
  }
}

class CountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      '${Provider.of<CountModel>(context).count}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

class PostCode extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// context からModelの値が使える
      '${Provider.of<CountModel>(context).address}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

まとめ

FlutterのProviderを使ってアプリのUIとロジックを分ける方法に付いて考えてきました。 普通に作るよりは見やすい作りで作れそうだという勘所を持つことができましたね。 ただ、根底にはやはりこれで良いのか?という気持ちもあります。

ま、気長にやっていきましょ。

 余談

子どもが生まれたお祝い金として市がコロナ支援金10万をくれるそうです。ありがてぇありがてぇ。