皆さん、StayHomeしていますか?
例年のGWなら、実家に行ったり飲みに行ったり旅行に出掛けたりして案外まとまった時間でコーディングできなかったりしますが、今年はどっぷりコーディングに憑かれそうですね(変換ミスだけど面白いのでそのままにしようw)
環境など
下記環境でFlutterアプリを開発中です。
ツールなど | バージョンなど |
---|---|
MacBook Air Early2015 | macOS Mojave 10.14.5 |
Android Studio | 3.6.1 |
Java | 1.8.0_131 |
Flutter | 1.12.13+hotfix.5 |
Dart | 2.7.0 |
上記のサイトなどを参考に、ChangeNotifierProvider
+Selector/Consumer
でAndroidのLiveData
っぽく使っていましたが、どうにもそれだけでは解決できないことがあって、その解決策として普通のProvider
を使う方法を試したので覚え書きです。
簡単な用語解説
概要というか私の乱暴な理解を書いておきます。厳密には違うことはあるでしょうが、この理解で一応実装上は困っていません。
1.BLoC
「Business Logic Component」の略。GoogleがFlutterアプリの設計アーキテクチャとして推奨していた。
開発するときは、以下の点を気をつける感じ。
- デバイス依存をBLoCクラスに入れない
- Stream/Sinkで描画データの更新/監視をする
- だからウィジェットは
StreamBuilder
を使う必要がある
- だからウィジェットは
私の場合は、まだ本格的な業務では携わったことが無いのもありますが、上記以外のことはあまり気にしないで使っています。
2.Provider
ChangeNotifierProvider
使っている人なら今更という所でしょうが、一応。
- providerパッケージを使う
- 基本的には、DI的な発想で、下位層にあるウィジェットに値を伝搬するコードの削減をするためのもの
- 色々種類がある
- ノーマルな
Provier
はインスタンスの生成と破棄を自動的にやってもらえて(※それぞれのメソッドを指定する)、下位Widgetで値を利用することが出来る。Selector/Consumer
である必要が無い- ただし、画面のルートウィジェット直下ではビルドメソッドでProviderが解決できないため、1つ子ウィジェットを挟む必要がある。詳細はProviderを使用できないタイミングを参照
-
StatefulWidget
を使うのをやめられる- 無駄なウィジェットリビルドを避けられる(=アプリ高速化,省メモリ化)
ノーマルなProvider
の使い方は、ChangeNotifierProvider
と全く同じです。
ウィジェットをProvider<MyBloc>
でくくり、下位のウィジェットからは、final bloc = Provider.of<Blocクラス>(context, listen: false);
でアクセスし、値の受け渡しなどが可能になります。
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: Provider<MyBloc>(
create: (context) => MyBloc(),
dispose: (context, bloc) => bloc.dispose(),
child: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Center(
child: Text("a"),
),
),
);
}
}
// MyHomePage内で使っているウィジェット
class SomeChildWidget extends StatelessWidget {
final bloc = Provider.of<MyBloc>(context, listen: false);
}
MyBloc
は、何かのクラスの派生である必要はありません。ここはChangeNotifierProvider
との違いですね。
class MyBloc {
...
}
やりたかったこと
戻るボタンの処理で、「編集していたら」値を返すというのを実現するため、Providerにアクセスしたい。
1. 戻るボタンのフック
まずは、戻るボタンのフック方法です。
WillPopScope
というのを使います。
class SampleScaffold extends StatelessWidget {
SampleScaffold({Key key, @required this.title}) : super(key: key);
final String title;
/// ページ全体
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: WillPopScope(
onWillPop: () {
return Future.value(true); // trueで戻る。falseで戻らない
},
child: ...,
);
}
}
これで、AppBarの戻るボタン、端末の戻るボタン両方拾えます。
以下でも大丈夫です。
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
return Future.value(true);
},
child: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: ...
),
);
2. ChangeNotifierProvider & Selector? Consumer?
それまでは、ChangeNotifierProvider
のみでこうしようとしていました。
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyViewModel>(
create: (context) => MyViewModel(),
child: SubPage(),
);
}
}
class SubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<MyViewModel, bool>(
selector: (context, model) => model.isModified,
builder: (context, date, child) =>
WillPopScope(
onWillPop: () {
_onBackPressed(context);
return Future.value(false);
},
child: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: ,
),
),
);
}
/// 画面戻り時の処理
void _onBackPressed(BuildContext context) {
final viewModel = Provider.of<MyViewModel>(context, listen: false);
Navigator.pop(context, viewModel.isModified);
}
}
class MyViewModel with ChangeNotifier {
Data data;
bool _isModified = false;
bool get isModified => _isModified;
void onDataUpdate(Data data){
// dataの更新処理
notifyListeners();
_isModified = true;
}
}
これだと、Scaffold
以下が、isModified
の値が変わるたびにリビルドされてしまいます。
Consumer
だろうとSelector
だろうと同じです。
よくありません。
全体のリビルドを避けたいから使っているのに・・・本末転倒です。
3. BLoC用のもう1つ別のProviderを作った
書いていて正直これはBLoCと言えないような気もしてきましたが、実装していたときはBLoC + Provider
にすればいいんじゃね?(ワクテカ)と思ってやっていたのでそのままにします。
_isModified
に関わる部分を移植しました。
class MyBloc{
bool _isModified = false;
bool get isModified => _isModified;
void onUpdate(){
_isModified = true;
}
}
さて、Providerが二つになりました。ChangeNotifierProvider
はそのまま残すからです。
なんでかって、StreamBuilderに書き変えるのが面倒だからです。
ChangeNotifierを使わず、BLoCパターンでウィジェットにデータ変更を流すためには、StreamBuilder
に変更が必要なんですが、今更変更箇所が多くて嫌です(キリッ)
なので、この二つのプロバイダーを同時に使いたい!
そうだ、マルチプロバイダーの出番だ!!
4. マルチプロバイダーにする
マルチプロバイダーは次のように設定します。
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<MyBloc>(
create: (context) => MyBloc(),
dispose: (context, bloc) => bloc.dispose()),
ChangeNotifierProvider<MyViewModel>(
create: (context) => MyViewModel(),
],
child: SubPage(),
);
}
}
これで、Consumer
やSelector
を使用する必要がなくなります。
class SubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
_onBackPressed(context);
return Future.value(false); // 上の関数で手動でpopしているのでfalseを返す
},
child: Scaffold(
...
),
}
}
_onBackPressed
では、Provider<MyBloc>
にアクセス出来ます。
void _onBackPressed(context){
final bloc = Provider.of<MyBloc>(context, listen: false);
Navigator.pop(context, bloc.isModified);
}
やった!
これで、遷移元にデータを変更したかどうかの結果を返すことが出来るようになりました。
ここで時間とやる気があれば、ChangeNotifier
なMyViewModel
の機能を全部MyBloc
に移して、Selector
やConsumer
で監視していたところをStreamBuilder
使うようにすれば、立派なBLoCパターンの出来上がるところですが、前述の通り、このままにします。
多分、設計としてはダメダメなんでしょうが、面倒だしマルチプロバイダーの例にもなるし、いいよね?
ウィジェットクラスのメンバー変数じゃダメなの?
と思ったあなた。
ダメなんです。
StatelessWidget
を使いたいから。
class SubPage extends StatelessWidget {
bool isModified = false
}
とすると、ステートレスなウィジェットが変更可能な値を持つのはよろしくないよ
と警告してくれます。
ビルドは一応通ったりもしますが、ダメと言われていることは回避するのが大人ってもんです。
だからといって、StatefulWidget
にするのは、やはりリビルド処理を減らしたいのに本末転倒です。
だから、Providerパターンが必要なんです。
Providerを使用できないタイミング
例)NGパターン
- Providerを宣言した
StatelessWidget/StatefulWidget
のbuild
メソッド内でProviderにアクセスしようとするのはNG
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Provider<MyBloc>(
create: (context) => MyBloc(),
dispose: (context, bloc) => bloc.dispose(),
child: _buildPage(context),
),
);
}
Widget _buildPage(BuildContext context){
// このタイミングではまだProviderが作成されておらず、blocはnullになる
final bloc = Provider.of<PermissionBloc>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: Text("title"),
),
body: Center(
child: Text("Hello!"),
),
);
}
}
例)OKパターン
- Providerを宣言した
StatelessWidget/StatefulWidget
から、一つ下位以降のStatelessWidget/StatefulWidget
のbuild
メソッドでは、Providerにアクセス可能。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Provider<MyBloc>(
create: (context) => MyBloc(),
dispose: (context, bloc) => bloc.dispose(),
child: MyHomePage(), // 下位のStatelessWidgetを挟む
),
);
}
}
class MyHomePage extends StatelessWidget{
@override
Widget build(BuildContext context) {
// このタイミングではProviderが作成済みでblocを利用できる
final bloc = Provider.of<MyBloc>(context, listen: false);
}
}
理由の想像ですが、多分、ProviderはInheritedWidget
を使いやすくラップしたものなので、一度親(Providerを宣言したウィジェット)のbuildを抜け(下位層のビルドが呼ばれ)ないと、ProviderがもつInheritedWidget
が作られないんでしょうね。(NGパターンの時は、そのウィジェットが持つInheritedWidget
がnullになっており、Providerが見つけられないというエラーが起こります)
参考サイト
長めだけどたぶんわかりやすいBLoCパターンの解説
https://qiita.com/kabochapo/items/8738223894fb74f952d3
Flutterでバックキーイベントを検知するには
https://kwmt27.net/2018/07/18/flutter-onbackpressed/