こんにちは、たかせです。Flutterアプリを作りまくってます。
早速ですが、今回はこれを作ります。
次の特徴を簡単に実現できるWidgetです。
- 任意の非同期処理を開始できる
- 非同期処理が走っている間、ボタンの見た目を変化させることができる
- 非同期処理が走っている間、他の開始命令を無視できる
progress_button等の既存ライブラリと比べると、手軽さを減らして汎用性を増やした作りになっています。アニメーション周りは自分で実装する必要がありますが、InkWellやGestureDetector、サードパーティー製の任意のボタンにも適用可能です。
3つ目の特徴である「非同期処理が走っている間、他の開始命令を無視できる」が割と便利だなーと感じていて、ボタン警察(見かけたボタンはすべて連打して不具合を報告してくれる人)を上司に持つ方には特におすすめです。
LazyFutureBuilderの説明
ソースコードです。
import 'package:flutter/material.dart';
class LazyFutureBuilder extends StatefulWidget {
final Future Function() futureBuilder;
final Widget Function(BuildContext context, Future Function() futureBuilder, bool isFutureBuilding) builder;
const LazyFutureBuilder({
@required this.futureBuilder,
@required this.builder,
});
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LazyFutureBuilder> {
var _isFutureBuilding = false;
@override
Widget build(BuildContext context) {
return widget.builder(
context,
() async {
if (_isFutureBuilding) {
return;
}
setState(() {
_isFutureBuilding = true;
});
try {
await widget.futureBuilder();
} finally {
setState(() {
_isFutureBuilding = false;
});
}
},
_isFutureBuilding,
);
}
}
こんなで1記事書くの?ってくらいシンプルです。
LazyFutureBuilderは2つのプロパティからなるWidgetで、その2つのプロパティはそれぞれ
- 排他的に処理したい非同期処理(を生成する関数)
- その非同期処理の開始タイミングを決めるWidget(を生成する関数)
を表現します。LazyFutureBuilderは、内部で「非同期処理を実行中かどうか」のフラグを管理しており、このフラグが立っている間は、非同期処理の開始をブロックします。
LazyFutureBuilderの使い方
ミニマムな使い方はこんな感じです。
LazyFutureBuilder(
futureBuilder: () async {
// 何らかの時間のかかる処理
await Future.delayed(const Duration(seconds: 2));
},
builder: (context, futureBuilder, isFutureBuilding) => RaisedButton(
onPressed: futureBuilder,
child: Text(
isFutureBuilding ? "ローディング中だよ!" : "連打してごらんよ",
),
),
),
ちょっとかっこよくするとこんな感じ。アニメーションにはAnimatedCrossFadeとflutter_spinkitを組み合わせています。
AniamtedCrossFade、雑に作ってもいい感じに動くという意味ですごく便利です。
LazyFutureBuilder(
futureBuilder: () async {
// 何らかの時間のかかる処理
await Future.delayed(const Duration(seconds: 2));
},
builder: (context, futureBuilder, isFutureBuilding) => RaisedButton(
color: Colors.indigo,
disabledColor: Colors.indigo,
textColor: Colors.white70,
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
onPressed: isFutureBuilding ? null : futureBuilder,
child: AnimatedCrossFade(
layoutBuilder: (first, _, second, __) => IntrinsicWidth(
child: IntrinsicHeight(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
first,
second,
],
),
),
),
firstChild: Text("ちょっとかっこよくなったよ"),
secondChild: SpinKitThreeBounce(
color: Colors.white70,
size: 24,
),
crossFadeState: isFutureBuilding ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
),
),
InkWellでも使い方は同じです。非同期処理中にアニメーションが不要な場合でも、ボタン警察への対策として手軽に使うことができます。
LazyFutureBuilder(
futureBuilder: () async {
// 何らかの時間のかかる処理
await Future.delayed(const Duration(seconds: 2));
},
builder: (context, futureBuilder, isFutureBuilding) => Align(
alignment: Alignment.centerRight,
child: InkWell(
customBorder: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: isFutureBuilding ? null : futureBuilder,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("もっと見る"),
Icon(
Icons.arrow_forward_ios,
size: 16,
),
],
),
),
),
),
),
ちなみに
providerを使っている場合は、LazyFutureBuilderの定義がもっと短くなります。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class LazyFutureBuilder extends StatelessWidget {
final Future Function() futureBuilder;
final Widget Function(BuildContext context, Future Function() futureBuilder, bool isFutureBuilding) builder;
const LazyFutureBuilder({
@required this.futureBuilder,
@required this.builder,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ValueNotifier<bool>>(
create: (context) => ValueNotifier<bool>(false),
child: Consumer<ValueNotifier<bool>>(
builder: (context, notifier, child) => builder(
context,
() async {
if (notifier.value) {
return;
}
try {
notifier.value = true;
await futureBuilder();
} finally {
notifier.value = false;
}
},
notifier.value,
),
),
);
}
}
すごいぞprovider。
もっとちなみに
2つ目の紹介したボタンを至るところに配置したくなったので、普段の開発ではLoadingCrossFadeという名前で別クラスを作って使ってます。
import 'package:collabo_base_app/resource/color_resource.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class LoadingCrossFade extends StatelessWidget {
final bool isLoading;
final Widget child;
const LoadingCrossFade({
@required this.isLoading,
@required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedCrossFade(
layoutBuilder: (first, _, second, __) => IntrinsicWidth(
child: IntrinsicHeight(
child: Stack(
alignment: Alignment.center,
children: [
first,
second,
],
),
),
),
crossFadeState: isLoading == true ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
firstChild: child,
secondChild: SpinKitThreeBounce(
color: Colors.white,
size: 16,
),
);
}
}
これを使えば、
LazyFutureBuilder(
futureBuilder: () async {
await Future.delayed(const Duration(seconds: 2));
},
builder: (context, futureBuilder, isFutureBuilding) => FlatButton(
color: Colors.indigo,
disabledColor: Colors.indigo,
textColor: Colors.white70,
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
onPressed: isFutureBuilding ? null : futureBuilder,
child: LoadingCrossFade(
child: Text("ちょっとかっこよくなったよ"),
isLoading: isFutureBuilding,
),
),
);
だいぶすっきりしますね。
おわります
環境
$ flutter doctor -v
[✓] Flutter (Channel dev, v1.14.6, on Mac OS X 10.14.5 18F132, locale ja-JP)
• Flutter version 1.14.6 at /~~~/Flutter/sdk/flutter
• Framework revision fabeb2a16f (5 days ago), 2020-01-28 07:56:51 -0800
• Engine revision c4229bfbba
• Dart version 2.8.0 (build 2.8.0-dev.5.0 fc3af737c7)