DEV Community

Femi-ige Muyiwa for Hackmamba

Posted on • Updated on

How to Create Tinder-like Swipe-able Cards in Flutter

Ever been fascinated by the scroll feature on dating applications like Tinder? Then you need to pay attention to this article!

In this tutorial, we want to show how to implement a swipe-able action similar to Tinder using Flutter and record every swipe action on a database. Also, we will use Appwrite's storage services to store the images (user images) and deliver them to the front end through their image URL.

Prerequisites

To fully grasp the concepts presented in this tutorial, we require the following:

Getting started

This section will give a rundown of the processes we will follow during this tutorial:

  • Creating a new Flutter project and connecting Appwrite to the Project.
  • Creating a new Appwrite document and storage.
  • Creating a swipe-able function (swipe up for superlike, swipe right for like, swipe left for dislike).
  • Create a List of image URLs.

Let‘s begin!

Creating a Flutter Project

To create a new flutter project, we will need to create a new directory, and we can do that by running the command below:

mkdir projectone
Enter fullscreen mode Exit fullscreen mode

After, we will type the command below in our terminal:

Flutter create tinder_app
Enter fullscreen mode Exit fullscreen mode

The command above creates a new flutter project called tinder_app; we can then navigate to the project folder.

Next, we want to install all the dependencies we will use during this Project.

Before we proceed, we will use a visual studio code extension called pubspec assist (or Flutter Enhancement Suite in Android studio) to add the packages. With this extension, we can successfully install any Flutter package by going to the command palette in Visual Studio code and selecting pubspec assist: Add/update dependencies. Once we click the option, we will type in the dependency's name. For this tutorial, we will make use of the following dependencies:

  1. Appwrite v6.0.0.
  2. Provider v6.0.3.
  3. Fluttertoast v8.0.9.

pubspec.yaml file

After installing all the dependencies, we will run the following command:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

This command saves the new packages in the pubspec.lock to ensure we get the same version of the package if we later run the command above.

Creating and Connecting Appwrite to our Flutter Project

Once we sign into the Appwrite console, we want to create a new project. To do that, we will need to start up our Appwrite instance by heading to the browser and typing in the hostname or IP address to open the Appwrite console. We will select Create Project within the console and give it a name and Project ID.

create project
project homepage

In Appwrite's home section, we will scroll down and create a new platform. In the platform section, we will select Android and input a name and a package name(note: We can find the package name in the app level build.gradle file).

Next, we will paste the code below into our AndroidManifest.xml file (note: To get to the file, select the android folder > app > src > main)

<manifest ...>
    ...
    <application ...>
    ...
    <!-- Add this inside the `<application>` tag, along side the existing `<activity>` tags -->
    <activity android:name="io.appwrite.views.CallbackActivity" android:exported="true">
        <intent-filter android:label="android_web_auth">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="appwrite-callback-[PROJECT_ID]" />
        </intent-filter>
    </activity>
    </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

Note: we will change project_ID to the ID we specified when creating Project

Next, navigate to the lib folder and create a new file called info.dart. Within this file, we will specify our project ID, database ID, and endpoint.

const projectid = "[REPLACE WITH PROJECT ID]";
const endpoint = "[REPLACE WITH ENPOINT]";
const database = "[REPLACE WITH COLLECTION ID]";
Enter fullscreen mode Exit fullscreen mode

For the endpoint, we will need to modify it to suit the device we will have running our Flutter project (physical emulator). Thus, to utilize our back end within our emulator, we will follow the steps below:

  • First, we will connect our emulator and the computer running our machine to the same Wi-Fi network.
  • Next, we will head to our terminal and type the command:
ipconfig
Enter fullscreen mode Exit fullscreen mode
  • This command will show all our computer connections and IP addresses.
  • We will then test out each IP address within our emulator until the right one redirects us to the sign-in page of the Appwrite console.

Note: This is not the only method to get the IP address of a device, but we can call it the most effective.

Creating an Appwrite Database and Storage

We want to create a new database document and storage within the console. Starting with the database document, we will follow the steps below:

  • We will select databases and fill in a new database name and ID.

new project home
create database

  • After, we create a new collection and give it a name and ID.

new collection

  • Next, we will head to the settings section within the collection to set the permissions to the document level permission (we do this because we will use an anonymous session).

permissions settings

  • Finally, we will create an attribute with the attribute ID message and type string. We will also set required to true, and then we're good to go.

creating attributes

To create our storage, we will head back to the home section, select storage, and click on add buckets to create a new bucket. Then, we'll set the bucket ID and name. After, we will add four image files to the bucket.

creating buckets
adding files

Creating the Tinder Project

To begin, we will clear out the code in the main.dart file and replace it with the code below:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tinder_app/cardprovider.dart';
import 'package:tinder_app/homepage.dart';

void main() {
    runApp(const MyApp());
}
class MyApp extends StatelessWidget {
    const MyApp({Key? key}) : super(key: key);
    static const String _title = 'Gallery App';
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) => ChangeNotifierProvider(
        create: (context) => CardProvider(),
        child: const MaterialApp(
        title: _title,
        debugShowCheckedModeBanner: false,
        home: Homepage(),
        ));
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we attached a ChangeNotifierProvider to extend the ChangeNotifier class CardProvider(). Next, we will create another file called homepage.dart. Within this file, we will create a stateful widget with the class name Homepage specified in the main.dart file. In this file, we will create a buildCard widget, and within this widget, we will return a button and a Stack widget linked to the function containing our image URL. When we click the button, it triggers the function and resets the image lists.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tinder_app/cardprovider.dart';
import 'package:tinder_app/tinder_card.dart';
class Homepage extends StatefulWidget {
    const Homepage({super.key});
    @override
    State<Homepage> createState() => _HomepageState();
}
class _HomepageState extends State<Homepage> {
    @override
    Widget build(BuildContext context) => Scaffold(
        backgroundColor: Colors.redAccent,
        body: SafeArea(
            child: Column(
            children: <Widget>[
            const SizedBox(
                height: 40,
            ),
            const Text.rich(TextSpan(children: [
                TextSpan(
                    text: 'Tinder',
                    style: TextStyle(
                    fontSize: 25,
                    fontWeight: FontWeight.w600,
                    color: Colors.black,
                    ),

                ),
                WidgetSpan(child: Icon(Icons.fireplace))
            ])),
            const SizedBox(
                height: 40,
            ),
            Expanded(
                child: Container(
                alignment: Alignment.center,
                padding: const EdgeInsets.all(16),
                child: buildCards(),
            )),
            ],
        )),
        );
    Widget buildCards() {
    final provider = Provider.of<CardProvider>(context);
    final assetImages = provider.assetImages;
    return assetImages.isEmpty
        ? Center(
            child: ElevatedButton(
                child: const Text('Restart'),
                onPressed: () {
                    final provider =
                        Provider.of<CardProvider>(context, listen: false);
                    provider.userimages();
                }))
        : Stack(
            children: assetImages
                .map((assetImage) => TinderCard(
                        assetImage: assetImage,
                        isFront: assetImages.last == assetImage,
                    ))
                .toList(),
            );
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, we will create a new file called tinder_card.dart. This file will get our images, the animation scroll direction, and the image position. To implement the functionality, we will need to wrap our card in a GestureDetector. With the help of the GestureDetector, we will be able to listen to three gestures:

  • onPanStart - When we start our gesture
  • onPanUpdate - When we drag image
  • onPanEnd - When we complete the interaction
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tinder_app/cardprovider.dart';
class TinderCard extends StatefulWidget {
    final String assetImage;
    final bool isFront;
    const TinderCard({
    Key? key,
    required this.assetImage,
    required this.isFront,
    }) : super(key: key);
    @override
    State<TinderCard> createState() => _TinderCardState();
}
class _TinderCardState extends State<TinderCard> {
    @override
    void initState() {
    super.initState();
    WidgetsBinding.instance!.addPostFrameCallback((_) {
        final size = MediaQuery.of(context).size;
        final provider = Provider.of<CardProvider>(context, listen: false);
        provider.setScreenSize(size);
    });
    }
    @override
    Widget build(BuildContext context) {
    return SizedBox.expand(
        child: widget.isFront ? buildfirstcard() : buildCard(),
    );
    }
    Widget buildfirstcard() => GestureDetector(
        child: LayoutBuilder(builder: (context, constraints) {
            final provider = Provider.of<CardProvider>(context);
            final position = provider.position;
            final milliseconds = provider.isDragging ? 0 : 400;
            final center = constraints.smallest.center(Offset.zero);
            final angle = provider.angle * pi / 180;
            final rotatedMatrix = Matrix4.identity()
            ..translate(center.dx, center.dy)
            ..rotateZ(angle)
            ..translate(-center.dx, -center.dy);
            return AnimatedContainer(
                curve: Curves.easeInOut,
                duration: Duration(microseconds: milliseconds),
                transform: rotatedMatrix..translate(position.dx, position.dy),
                child: buildCard());
        }),
        onPanStart: (details) {
            final provider = Provider.of<CardProvider>(context, listen: false);
            provider.startPosition(details);
        },
        onPanUpdate: (details) {
            final provider = Provider.of<CardProvider>(context, listen: false);
            provider.updatePosition(details);
        },
        onPanEnd: (details) {
            final provider = Provider.of<CardProvider>(context, listen: false);
            provider.endPosition();
        },
        );
    Widget buildCard() {
    return ClipRRect(
        borderRadius: BorderRadius.circular(20),
        child: Container(
        decoration: BoxDecoration(
            image: DecorationImage(
            image: NetworkImage(widget.assetImage),
            fit: BoxFit.cover,
            // alignment: const Alignment(-0.3, 0)
        )),
        ),
    );
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, we will get some details and we want to put the details in a provider (note: the provider will serve as state management). We will use this provider by getting a reference to it and redirecting the details into the methods above.

Finally, we will create a file called cardprovider.dart, which is the file that extends the ChangeNotifier to the main.dart build method. In the file, we will create the three methods mentioned in the gesture detector widgets in the code above. This file will handle the saving of our swipe position and the swipe status (e.g., like, dislike, superlike) using fluttertoast. It will also address the Appwrite initialization, image URL list, anonymous session creation, and database recording.

import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:appwrite/appwrite.dart';
import 'package:tinder_app/api/info.dart';
enum CardStatus { like, dislike, superlike }
class CardProvider extends ChangeNotifier {
    List<String> _assetImages = [];
    bool _isDragging = false;
    double _angle = 0;
    Offset _position = Offset.zero;
    Size _screenSize = Size.zero;
    late final Client _client;
    late final Account _account;
    late final Databases _databases;
    List<String> get assetImages => _assetImages;
    bool get isDragging => _isDragging;
    Offset get position => _position;
    double get angle => _angle;
    CardProvider() {
    initialize();
    }
    void setScreenSize(Size screenSize) => _screenSize = screenSize;
    void startPosition(DragStartDetails details) {
    _isDragging = true;
    notifyListeners();
    }
    void updatePosition(DragUpdateDetails details) {
    _position += details.delta;
    final x = _position.dx;
    _angle = 45 * x / _screenSize.width;
    notifyListeners();
    }
    void endPosition() async {
    _isDragging = false;
    notifyListeners();
    final status = getStatus();
    final check = status.toString().split('.').last.toUpperCase();
    if (status != null) {
        Fluttertoast.cancel();
        Fluttertoast.showToast(
        msg: check,
        fontSize: 36,
        );
        try {
        await _databases.createDocument(
            collectionId: 'images',
            documentId: 'unique()',
            data: {'message': check},
        );
        } catch (e) {
        debugPrint('Eror while creating record:$e');
        }
    }
    switch (status) {
        case CardStatus.like:
        like();
        break;
        case CardStatus.dislike:
        dislike();
        break;
        case CardStatus.superlike:
        superlike();
        break;
        default:
        resetPosition();
    }
    }
    void resetPosition() {
    _isDragging = false;
    _position = Offset.zero;
    _angle = 0;
    notifyListeners();
    }
    CardStatus? getStatus() {
    final x = _position.dx;
    final y = _position.dy;
    final forceSuperLike = x.abs() < 20;
    final delta = 100;
    if (x >= delta) {
        return CardStatus.like;
    } else if (x <= -delta) {
        return CardStatus.dislike;
    } else if (y <= -delta / 2 && forceSuperLike) {
        return CardStatus.superlike;
    }
    }
    void dislike() {
    _angle = -20;
    _position -= Offset(2 * _screenSize.width, 0);
    _nextImage();
    notifyListeners();
    }
    void like() {
    _angle = 20;
    _position += Offset(2 * _screenSize.width, 0);
    _nextImage();
    notifyListeners();
    }
    void superlike() {
    _angle = 0;
    _position -= Offset(0, _screenSize.height);
    _nextImage();
    notifyListeners();
    }
    Future _nextImage() async {
    if (_assetImages.isEmpty) return;
    await Future.delayed(const Duration(milliseconds: 200));
    _assetImages.removeLast();
    resetPosition();
    }
    void initialize() {
    _client = Client()
        ..setEndpoint(endpoint)
        ..setProject(projectid);
    _account = Account(_client);
    _databases = Databases(_client, databaseId: databaseid);
    userimages();
    }
    void userimages() async {
    try {
        await _account.get();
    } catch (_) {
        await _account.createAnonymousSession();
    }
    _assetImages = <String>[
        'http://[hostname or ip address]/v1/storage/buckets/images/files/4/preview?project=tinder',
        'http://[hostname or ip address]/v1/storage/buckets/images/files/2/preview?project=tinder',
        'http://[hostname or ip address]/v1/storage/buckets/images/files/3/preview?project=tinder',
        'http://[hostname or ip address]/v1/storage/buckets/images/files/1/preview?project=tinder',
    ].reversed.toList();
    notifyListeners();
    }
}
Enter fullscreen mode Exit fullscreen mode

The code above enables that when we start our application, it will try to get any available session, and if there is no session, it will create an anonymous session. To register each swipeable action, we created an if statement to check if the status is not null. If the answer is true, it will show a toast message and create a new database document, our toast message.

Testing the Application

To test the application, we will run the command below in our terminal:

flutter run
Enter fullscreen mode Exit fullscreen mode

At the moment, this is what our application looks like:

result

Conclusion

We have concluded this tutorial on creating a swipe-able card using Flutter and Appwrite to record each swipe action. Here are a few additional resources to assist with the learning process:

Thanks for reading and happy coding!

Top comments (1)

Collapse
 
mrk8799 profile image
Mr.k

in release mode tinder swipe not working properly in first iteration but when we click on restart then swipe then working fine please let me how to correct this in release mode
thank you