FlutterでProviderパターンを使ってUIとロジックを分離する方法
はじめに
Flutterでアプリを開発する際に、UIとロジックを適切に分離して管理することが重要です。Providerパターンを利用することで、この目的を達成することができます。
この記事では、FlutterでProviderパターンを使用してUIとロジックを分離する方法を説明します。
嬉しさ
大規模アプリを開発する場合、いきあたりばったりで作り込んでいくと機能を追加することに苦痛を伴います。 そこでGoogle社は、BloC Patternを使うことが2018年ぐらいに推奨していました。
ただし、個人で作るアプリにBLoC Patternはやりすぎ(らしい)。 かと言って何も何もないとUIとロジックがぐちゃぐちゃになります。
実装
1時間ぐらいネットの記事読んで理解したつもりになり書き始めたので正しいのか疑問ですが…書いてみましょう。
急ぐ人向け
とりあえず動かしてみたい人向けにgithubレポジトリに置いときました。 適当にやってください。
git clone https://github.com/kenpos/providerPatternSample.git
flutter pub get
flutter run
コード解説
この記事では、以下のようなシンプルなアプリを例にして、Providerパターンを使ってUIとロジックを分離する方法を解説していきます。
- 画面上には、「ウマ娘」と表示されるテキストボックス(赤枠)と、カウント数を表示するテキストボックス(青枠)があります。
- 右下にあるfloatingActionButton(黄色枠)を押すと、カウント数が増え、カウント数が5を超えると、「ウマ娘」の文字列が「ウマ娘プリティーダービー」に変わります。
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とロジックを分ける方法を解説しました。この方法を使うことで、コードが見やすく、保守しやすいアプリを開発することができます。ただし、どのような方法が最適かはアプリの規模や要件によって変わるため、適切なアプローチを選択することが重要です。