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:
- Docker Installation(recommended), DigitalOcean droplet, or Gitpod.
- An Appwrite instance. Check out this article 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:
mkdir projectone
After, we will type the command below in our terminal:
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:
After installing all the dependencies, we will run the following command:
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
.
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>
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]";
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
- 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.
- After, we create a new collection and give it a name and ID.
- 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).
- 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.
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 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(),
));
}
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(),
);
}
}
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)
)),
),
);
}
}
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();
}
}
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
At the moment, this is what our application looks like:
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!