LoginSignup
21
13

More than 3 years have passed since last update.

どんなボタンにも対応できるprogress_button

Last updated at Posted at 2020-02-02

こんにちは、たかせです。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つのプロパティはそれぞれ

  1. 排他的に処理したい非同期処理(を生成する関数)
  2. その非同期処理の開始タイミングを決めるWidget(を生成する関数)

を表現します。LazyFutureBuilderは、内部で「非同期処理を実行中かどうか」のフラグを管理しており、このフラグが立っている間は、非同期処理の開始をブロックします。

LazyFutureBuilderの使い方

ミニマムな使い方はこんな感じです。

1opt.gif

LazyFutureBuilder(
  futureBuilder: () async {
    // 何らかの時間のかかる処理
    await Future.delayed(const Duration(seconds: 2));
  },
  builder: (context, futureBuilder, isFutureBuilding) => RaisedButton(
    onPressed: futureBuilder,
    child: Text(
      isFutureBuilding ? "ローディング中だよ!" : "連打してごらんよ",
    ),
  ),
),

ちょっとかっこよくするとこんな感じ。アニメーションにはAnimatedCrossFadeflutter_spinkitを組み合わせています。

AniamtedCrossFade、雑に作ってもいい感じに動くという意味ですごく便利です。

2opt.gif

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でも使い方は同じです。非同期処理中にアニメーションが不要な場合でも、ボタン警察への対策として手軽に使うことができます。

3opt.gif

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)

21
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
13