🎴

Flutter で Tinder UI を実装する

7 min read

このページでは Flutter で Tinder アプリのような、左右にカードをスワイプする UI の実装方法を紹介します。

記事を書く動機

先日、Tinder UI を実装するためのライブラリをリリースしたので、宣伝を兼ねて使用方法を記事として残しておこうと思いました。

https://twitter.com/heavenOSK/status/1417454036891029506

以下のライブラリです。この記事を参考に使ってみてください。

実装方法

pubspec.yaml に swipeable_stack を追加します。

dependencies:
  flutter:
    sdk: flutter
  swipeable_stack: # <- 追加

似た名前の swipable_stack というライブラリとお間違えないようご注意ください。以前に実装したもので、根本的な設計を見直して swipeable_stack で再実装しました。

データの準備

表示したいカードのデータを準備します。

swipeable_stack で使用するカードのデータは SwipeableStackIdentifiable というクラスを継承している必要があります。このクラスはシンプルなクラスで id という getter を持っているだけです。
swipeable_stack では id を使ってカードを一意に識別して、不整合が起きないようにしています。

Tinder のようなユーザー同士のマッチングアプリでは、カードにユーザー情報を表示するので以下のようなクラスを用意します。

class IdentifiableUser extends SwipeableStackIdentifiable {
  IdentifiableUser({
    this.user,
  });

  final User user;

  
  String get id => user.id; // <- ユーザーの ID を返すようにしておく
}

実際のサービスで実装する際は、SwipeableStack に受け渡す際以下のような変換コードを使用することになります。

final users = <User>[];
final identifiableUsers = users
    .map(
      (user) => IdentifiableUser(user: user),
    )
    .toList();

カードの表示

データの受け渡し

上記で用意した SwipeableStackIdentifiable を継承したクラスの一覧を SwipeableStack.dataSet に渡します。

class MyPage extends StatelessWidget {
  const MyPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final identifiableUsers = <IdentifiableUser>[
      IdentifiableUser(
        user: userA,
      ),
      // 必要に応じてデータを追加する
    ];
    return Scaffold(
      body: SwipeableStack<IdentifiableUser>(
        dataSet: identifiableUsers,
      ),
    );
  }
}

カードを表示する

カードの表示には SwipeableStack.builder を使用します。buidler 内で受け渡されたデータを参照することができるので、それらの情報を使用してカードを表示します。

SwipeableStack<IdentifiableUser>(
  dataSet: identifiableUsers,
  builder: (context, IdentifiableUser data, constraints) {
    return Center(
      child: Container(
        width: constraints.maxWidth * 0.9,
        height: constraints.maxHeight * 0.8,
        color: Colors.blue,
        alignment: Alignment.center,
        child: Text(data.user.name),
      ),
    );
  },
);

SwipeableStack を使用する際には使用するデータのクラスをジェネリクスで指定しておく必要があります。
指定を忘れると builder 内で必要な情報が参照できないのでご注意ください。

SwipeableStack<IdentifiableUser>( // <- <IdentifiableUser> を指定する
    dataSet: identifiableUsers,
),

完了時コールバック

スワイプが完了した際の処理は SwipeableStack.onSwipeCompleted で行います。マッチングアプリだと「Like/Nope したことをバックエンドに保存する」等の処理です。

コールバック内では、対象のカードの data とスワイプの方向(SwipeDirection)を使用することができます。

SwipeableStack<IdentifiableUser>(
  onSwipeCompleted: (data, direction) {
    // 対象のカードで使用しているデータを使用することができます。
    final targetUserId = data.id;
    final targetUser = data.user;
    // スワイプ方向それぞれに処理を記述します。
    switch (direction) {
      case SwipeDirection.left:
      // TODO: 左方向にスワイプした場合の処理
        break;
      case SwipeDirection.right:
      // TODO: 右方向にスワイプした場合の処理
        break;
      case SwipeDirection.up:
      // TODO: 上方向にスワイプした場合の処理
        break;
      case SwipeDirection.down:
      // TODO: 下方向にスワイプした場合の処理
        break;
    }
  },
);

onWillMoveNext コールバック

SwipeableStack では、スワイプ処理が行われる直前に呼ばれる onWillMoveNext コールバックがあります。
このコールバックは、ユーザーのスワイプ操作を許可・不許可を操作する目的で使用します。許可した場合はスワイプアニメーションが行われ、不許可にした場合はスワイプアクションがキャンセルされます。

コールバック内では対象のカードの data とスワイプ方向を取得することができ、「スワイプを許可する場合は true」「スワイプを不許可にする場合は false」を返すことで制御できます。

特定のスワイプ方向のみを許可する場合は、コールバック内で SwipeDirection を判定して bool 値を返します。次のコードでは、横方向のスワイプのみを許可しています。

SwipeableStack<IdentifiableUser>(
  onWillMoveNext: (data, direction) {
    final allowedDirection = [
      SwipeDirection.right,
      SwipeDirection.left
    ];
    return allowedDirection.contains(direction);
  },
);

その他、よく使用すると思われる用途のコード例を記載します。

ex1. ユーザーの状態に応じてスワイプを不許可にする

ユーザーのポイント不足など、特定の条件でスワイプを不許可にしたい場合は以下の様なコードになります。

SwipeableStack<IdentifiableUser>(
  onWillMoveNext: (data, direction) {
    final myPoint = 0; // <- 取得方法は各アプリの設計によって異なります
    if (myPoint <= 0) {
      return false;
    }
    final allowedDirection = [
      SwipeDirection.right,
      SwipeDirection.left
    ];
    return allowedDirection.contains(direction);
  },
);
ex2. 不許可の場合にダイアログを表示する

false を返す直前に addPostFrameCallback を使用して、ダイアログを呼び出します。

SwipeableStack<IdentifiableUser>(
  onWillMoveNext: (data, direction) {
    final myPoint = 0; 
    if (myPoint <= 0) {
      WidgetsBinding.instance!.addPostFrameCallback((_) {
        showDialog<void>(
            context: context,
            builder: (ctx) {
              return AlertDialog(
                content: Text('ポイント不足です'),
              );
            }
        );
      });
      return false;
    }
    final allowedDirection = [
      SwipeDirection.right,
      SwipeDirection.left
    ];
    return allowedDirection.contains(direction);
  },
);

スワイプを操作する

スワイプジェスチャー以外でカードを操作したい時は、SwipeableStackController を使用します。

操作したい SwipeableStackSwipeableStackController を受け渡します。

class _HomeState extends State<Home> {
  late final _controller = SwipeableStackController<IdentifiableUser>();

  
  Widget build(BuildContext context) {
    return SwipeableStack<IdentifiableUser>(
      controller: _controller,
      // ...
    );
  }
}

次のように next メソッドを呼び出すことで、引数で受け渡したスワイプ方向にカードを操作して次のカードに進むことができます。

_controller.next(
  swipeDirection: SwipeDirection.right,
);

rewindを使えば一つ前のカードにアニメーション付きで巻き戻すこともできます。

_controller.rewind();

オーバーレイの表示

オーバーレイを表示するには SwipeableStack.overlayBuilder を使用します。directionswipeProgress (スワイプ幅) が使用できるので、方向に応じて適切なオーバーレイを表示することができます。

SwipeableStack<IdentifiableUser>(
  overlayBuilder: (
    context,
    constraints,
    data,
    direction,
    swipeProgress,
  ) {
    final opacity = math.min<double>(swipeProgress, 1);
    final isRight = direction == SwipeDirection.right;
    final isLeft = direction == SwipeDirection.left;

    return Padding(
      padding: const EdgeInsets.all(48),
      child: Stack(
        children: [
          Opacity(
            opacity: isRight ? opacity : 0,
            child: CardLabel.right(),
          ),
          Opacity(
            opacity: isLeft ? opacity : 0,
            child: CardLabel.left(),
          ),
        ],
      ),
    );
  },
);

おわりに

今後も追記していこうと思います。

何かご質問や不明点があればプロフィールから Twitter にメンションください。

バグを見つけていただいた方は Github Issues までお願いします。

この記事に贈られたバッジ