How to Create Tinder-like Swipe-able Cards in Flutter
Tags: flutter, appwrite, tinder, hackmamba
Published: 2022-09-29 13:20:39

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](https://appwrite.io/?utm_source=hackmamba&utm_medium=hackmamba-blog) 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:

- Docker Installation(recommended), [DigitalOcean droplet](https://marketplace.digitalocean.com/apps/appwrite), or [Gitpod](https://gitpod.io/#https://github.com/appwrite/integration-for-gitpod).
- An Appwrite instance. Check out this [article ](https://dev.to/hackmamba/create-a-local-appwrite-instance-in-3-steps-19n9) for the setup.


## 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:

```bash
mkdir projectone
```

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

```bash
Flutter create tinder_app
```

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.](http://https://pub.dev/packages/appwrite/versions/6.0.0)
2. [Provider v6.0.3.](https://pub.dev/packages/provider)
3. [Fluttertoast v8.0.9.](https://pub.dev/packages/fluttertoast)

![pubspec.yaml file](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661704186328_Screenshot+40.png)

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

```bash
flutter pub get
```

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](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661702124156_Screenshot+39.png)
![project homepage](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661702124197_Screenshot+38.png)

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`)

```xml
<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>
```

> 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.

```dart
const projectid = "[REPLACE WITH PROJECT ID]";
const endpoint = "[REPLACE WITH ENPOINT]";
const database = "[REPLACE WITH COLLECTION ID]";
```

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:

```bash
ipconfig
```

- 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](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661558972969_Screenshot+27.png)
![create database](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661558973104_Screenshot+28.png)

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

![new collection](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661559041053_Screenshot+29.png)

- 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](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661559058613_Screenshot+30.png)

- 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](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661559187270_Screenshot+31.png)

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](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661559340449_Screenshot+32.png)
![adding files](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661559340477_Screenshot+33.png)

### Creating the Tinder Project

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

```dart
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(),
        ));
}
```

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.

```dart
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(),
            );
    }
}
```

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

```dart
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)
        )),
        ),
    );
    }
}
```

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.

```dart
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();
    }
}
```

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:

```bash
flutter run
```

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


![result](https://paper-attachments.dropbox.com/s_CA6DF0FF9AD4E047DDF348FD065E707966559831DCBB483B4C898BD403CB0C31_1661561242074_ezgif.com-gif-maker+2.gif)

## 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:


- [Appwrites databases API](https://appwrite.io/docs/databases)
- [Flutter null safety](https://dart.dev/null-safety)
- [Appwrite Flutter SDK](https://appwrite.io/docs/getting-started-for-flutter?utm_source=hackmamba&utm_medium=hackmamba-blog)

Thanks for reading and happy coding!