【Flutter】マッチング対象の選り分けをスワイプ操作で直感的に行う方法

こんにちは、株式会社Pentagonでアプリ開発をしている中原です。

マッチングアプリでは、ユーザーが自分の好みに基づいてプロフィールを選ぶマッチング機能があります。この機能は、ユーザーが相手のプロフィールを左右(嫌い、好き)にスワイプすることで、相手に興味があることを示し、次のプロフィールを表示します。また、スワイプ操作は簡単で直感的な操作であるため、ユーザーはすぐに理解し利用することができます。これにより、多くのプロフィールから相手を選ぶことが可能となり、マッチングの可能性が向上します。その結果、使いやすさとユーザー体験が向上します。この記事では、そのようなスワイプ(上下左右)可能なウィジェットの実装方法について説明します。

【こんな人に読んで欲しい】

  • Flutterアプリ開発エンジニア。
  • マッチング機能のようなスワイプ(上下左右)可能なウィジェットを実装したい方

【この記事を読むメリット】

  • スワイプ(上下左右)可能なウィジェットの実装方法を学べます。

【結論】
appinio_swiperパッケージを追加し、AppinioSwiper()ウィジェット引数のcardsBuilderに表示したいウィジェットを指定するだけで、そのウィジェットを上下左右にスワイプすることが可能となります。また、AppinioSwiper()ウィジェット引数のcontrollerAppinioSwiperControllerを指定することで、そのウィジェットのスワイプ方向を自由に制御することも可能です。

目次

この記事で触れないこと

この記事では、以下の実装や説明については省略しています。

  • Flutterの開発環境構築
  • AsyncNotifierProviderの説明
  • Freezedの説明

開発環境

  • Flutter 3.10.6
  • Dart 3.0.6
  • vscode

パッケージ

dependencies:
  flutter:
    sdk: flutter
  appinio_swiper: ^2.0.3 // パッケージを追加
  flutter_riverpod: ^2.4.0
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  build_runner: ^2.4.6
  freezed: ^2.4.3
  json_serializable: ^6.7.1

画像の準備

  1. assetsフォルダを作成します。
  2. assetsフォルダの中にimagesフォルダを作成します。
  3. 画像ファイルのflutter.pngimagesフォルダに挿入します。画像ファイルにはお好きな画像を設定してください。
  4. pubspec.yamlに以下の設定を追加します。
flutter:
  uses-material-design: true

// 追加
  assets:
    - assets/images/

Userモデルの実装

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';

@freezed
class User with _$User {
  const factory User({
    @Default([]) List<String> profileImageURL,
    @Default("") String name,
  }) = _User;

  const User._();
}

ViewModelの実装

import 'dart:async';

import 'package:appinio_swiper/appinio_swiper.dart';
import 'package:card_swiper/user.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final swipeAsyncNotifierProvider =
    AsyncNotifierProvider<SwipeAsyncNotifier, List<User>>(
        SwipeAsyncNotifier.new);

class SwipeAsyncNotifier extends AsyncNotifier<List<User>> {
  @override

  // 初期値にUserデータを生成
  FutureOr<List<User>> build() {
    return List<User>.generate(5, (index) {
      return User(
        profileImageURL: ["assets/images/flutter.png"],
        name: "ジョン${index + 1}",
      );
    });
  }

  // スワイプ時の処理
  Future<void> swipeOnCard(
    AppinioSwiperDirection direction,
  ) async {
    switch (direction) {
      case AppinioSwiperDirection.left: // 左方向
        // 左方向にスワイプした時の処理
        break;
      case AppinioSwiperDirection.right: // 右方向
        // 右方向にスワイプした時の処理
        break;
      case AppinioSwiperDirection.top: // 上方向
        // 上方向にスワイプした時の処理
        break;
      case AppinioSwiperDirection.bottom: // 下方向
        // 下方向にスワイプした時の処理
        break;
      default:
    }
  }
}

初期値にウィジェットに表示するUserデータを生成しています。swipeOnCardメソッドには、スワイプした方向に応じて何かしらの処理を実行する処理が書かれています。

カードコンポーネントの実装

import 'package:appinio_swiper/appinio_swiper.dart';
import 'package:card_swiper/user.dart';
import 'package:flutter/material.dart';

class SwipeCard extends StatelessWidget {
  const SwipeCard({
    super.key,
    required this.list,
    required this.onSwiping,
    required this.controller,
  });

  final List<User> list;
  final AppinioSwiperController controller;
  final void Function(AppinioSwiperDirection direction) onSwiping;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: AppinioSwiper(
            controller: controller, // スワイプを制御するコントローラー
            cardsCount: list.length, // カードの数
            onSwiping: onSwiping, // スワイプ中の処理
            cardsBuilder: (BuildContext context, int index) {
              final user = list[index];
              return list.isNotEmpty
                  ? _buildCard(user: user) // カード型ウィジェット
                  : Center(child: _buildText("No Data"));
            },
          ),
        ),
        _buildActionButton(controller), // アクションボタン
        const SizedBox(height: 10),
      ],
    );
  }

  // カード型コンポーネント
  Widget _buildCard({required User user}) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: Colors.black, width: 1),
        image: DecorationImage(
            image: AssetImage(user.profileImageURL[0]),
            fit: BoxFit.cover,
            alignment: Alignment.center),
      ),
      child: Container(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildText(user.name),
          ],
        ),
      ),
    );
  }

  // アクションボタンコンポーネント
  Widget _buildActionButton(AppinioSwiperController controller) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildCustomBtn(
          onPressed: () {
            controller.swipeLeft(); // 左方向にスワイプ
            // 左方向にスワイプした時の処理
          },
          iconData: Icons.cancel,
          color: Colors.red,
        ),
        _buildCustomBtn(
          onPressed: () {
            controller.swipeUp(); // 上方向にスワイプ
            // 上方向にスワイプした時の処理
          },
          iconData: Icons.star,
          color: Colors.blue,
        ),
        _buildCustomBtn(
          onPressed: () {
            controller.swipeRight(); // 右方向にスワイプ
            // 右方向にスワイプした時の処理
          },
          iconData: Icons.favorite,
          color: Colors.teal,
        ),
      ],
    );
  }

  // ボタンコンポーネント
  Widget _buildCustomBtn({
    required void Function()? onPressed,
    required IconData iconData,
    required Color color,
  }) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        foregroundColor: Colors.black,
        backgroundColor: Colors.white,
        elevation: 8,
        shape: const CircleBorder(),
        minimumSize: const Size.square(50),
      ),
      child: Icon(
        iconData,
        color: color,
      ),
    );
  }

  // テキストコンポーネント
  Widget _buildText(String text) {
    return Text(
      text,
      style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
    );
  }
}

カードコンポーネント、テキスト、ボタンなどの各種コンポーネントを作成します。AppinioSwiper()ウィジェットはcardsBuilder引数にウィジェットを指定することで、上下左右自由にスワイプできるウィジェットを表示することができます。また、AppinioSwiper()ウィジェットのcontroller引数にAppinioSwiperControllerを指定することで、指定方向のスワイプを制御することが可能となります。

画面の実装

import 'package:appinio_swiper/appinio_swiper.dart';
import 'package:card_swiper/swipe_card.dart';
import 'package:card_swiper/swipe_notifer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerStatefulWidget {
  const MainApp({super.key});

  @override
  ConsumerState<MainApp> createState() => _MainAppState();
}

class _MainAppState extends ConsumerState<MainApp> {
  late AppinioSwiperController _swiperController;

  @override
  void initState() {
    super.initState();
    // コントローラーの初期化
    _swiperController = AppinioSwiperController();
  }

  @override
  void dispose() {
    super.dispose();
    // コントローラーの破棄
    _swiperController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Userデータの取得
    final asyncValue = ref.watch(swipeAsyncNotifierProvider);
    // ViewModelの取得
    final notifier = ref.read(swipeAsyncNotifierProvider.notifier);
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(
          child: asyncValue.when(
            // ローディング中の処理
            loading: () => const Center(
              child: CircularProgressIndicator(),
            ),
            // データ時の処理
            error: (error, _) => const Center(
              child: Text("Error"),
            ),
            // データ取得後の処理
            data: (data) {
              // カードウィジェットの表示
              return SwipeCard(
                list: data,
                controller: _swiperController, // スワイプ制御
                onSwiping: (AppinioSwiperDirection direction) async =>
                    await notifier.swipeOnCard(direction), // スワイプ処理
              );
            },
          ),
        ),
      ),
    );
  }
}

AppinioSwiperControllerの初期化や破棄、WigetRefを使用するため、ConsumerStatefulWidgetを使用しています。指でタップすることにより、上下左右にスワイプ可能なことが確認できます。さらに、カードウィジェットの下部にあるボタンにはswiperControllerが割り当てられており、左、上、右へのスワイプ処理が実装されています。これによりスワイプの制御が可能となっています。

まとめ

appinio_swiperパッケージを使用すると、スワイプ(上下左右)可能なウィジェットを簡単に実装することができます。この記事では詳細な実装は行っていませんが、自由にカスタマイズ可能です。あなたのアプリに最適な形に調整してみてください。

採用情報はこちら
目次