How to Make a Tinder-Like Dating App
How to Vibe Code a Tinder-Like Dating App on Android
Stay in your creative flow. Direct AI models with high-level matchmaking intent, stitch together a Jetpack Compose swipe deck with card-tilt physics, LIKE/NOPE stamps, and mutual match modals, and run it on your phone in under 30 minutes.
Core Stack
Compose Gestures
Physics Engine
Drag + RotationZ
AI Partner
Claude / Gemini
Target Time
20β30 Mins
The Vibe Coding Mobile Philosophy
Historically, building a Tinder-style card deck on Android meant importing abandoned GitHub XML libraries or subclassing ItemTouchHelper with glitchy z-index stacking.
Vibe coding makes swipe physics effortless: you describe the card tilt, stamp thresholds, and match celebration modal, while conversational AI writes declarative Compose pointerInput gestures and graphicsLayer transforms.
Old-School Mobile Stress
- Fighting abandoned third-party RecyclerView swipe deck libraries.
- Janky card transitions when the top card is flung off-screen.
- Writing boilerplate SQLite tables just to test mutual match popups.
- Manually calculating touch velocity and rotation angles in legacy Views.
The Vibe Coder Way
- Prompt AI for a stacked Compose card deck with live drag offset & tilt.
- Display dynamic "LIKE" and "NOPE" stamps as the user drags left or right.
- Trigger an instant "It's a Match!" celebration overlay and matches bar.
- Rewind the last swipe with a 1-tap undo history stack.
The 4-Step Blueprint
From a blank Android Studio project to a tactile swipe-and-match experience.
Empty Compose Setup
Start a new Empty Activity (Compose) project in Android Studio named VibeSpark. Compose's built-in gesture detectors handle card dragging natively.
Modifier.graphicsLayer and detectDragGestures give you full control over card physics.
Configure Your Matchmaker
Pick your dating aesthetic, swipe physics, and match flow in the configurator below, then paste the generated prompt into Claude or Gemini.
topProfile.id as the key to pointerInput(topProfile.id) so drag state resets cleanly when the top card changes.
Single-File Drop-In
Paste the generated code into MainActivity.kt to test finger-drag physics, Rewind/Pass/Like buttons, and mutual match modals immediately.
Add Filters & Icebreaker Chat
Use the Prompt Vault below to add multi-photo tap carousels, distance/age filter sheets, and 1-on-1 match chat threads.
Vibe Prompt Configurator
Select your dating app aesthetic, card physics engine, and matchmaking features. Copy the generated prompt directly into Claude, Cursor, or Gemini.
Essential Elements of a Tinder-Like Dating App
Modern matchmaking apps combine expressive user profiles with low-friction discovery and safe communication. Make sure your app covers these 6 essentials:
-
Rich User Profiles & Interest Tags
Display name, age, proximity in kilometers/miles, a concise personality bio, and shared hobby badges so users can make informed swipe decisions.
-
Location & Preference Discovery
Allow users to filter potential matches by maximum distance, age range, and shared interests so every profile card feels relevant.
-
Swipe Deck & Mutual Match Celebration
Pair physical horizontal drag gestures (Right = Like, Left = Pass) with an exciting "It's a Match!" celebration modal when both users like each other.
-
Real-Time Chat & Icebreakers
Unlock private messaging once a mutual match occurs, complete with 1-tap icebreaker prompts tailored to shared interests.
-
Voice & Video Date Readiness
Support in-app audio notes or video vibe checks so matched users can verify chemistry safely before meeting in person.
-
Rewind, Super Like & Notifications
Include a "Rewind Last Swipe" undo stack for accidental passes alongside standout Super Likes and instant match notifications.
The "Golden" Single-File Compose Template
Replace the contents of your MainActivity.kt with this to run a working Swipe-and-Match Dating App right now.
package com.example.vibedating
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.math.abs
data class DatingProfile(
val id: Int,
val name: String,
val age: Int,
val distanceKm: Int,
val bio: String,
val interests: List<String>,
val gradientColors: List<Color>,
val likesYou: Boolean
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFF43F5E),
background = Color(0xFF0A0D14),
surface = Color(0xFF161B28)
)
) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
DatingSwipeScreen()
}
}
}
}
}
@Composable
fun DatingSwipeScreen() {
val initialDeck = remember {
listOf(
DatingProfile(
1, "Elena", 26, 3,
"Product designer by day, espresso tonic enthusiast & vinyl collector by night.",
listOf("Design", "Vinyl", "Climbing"),
listOf(Color(0xFF831843), Color(0xFFE11D48), Color(0xFFF97316)),
likesYou = true
),
DatingProfile(
2, "Liam", 28, 5,
"Building indie Android apps and hunting for the best ramen spot in the city.",
listOf("Kotlin", "Ramen", "Running"),
listOf(Color(0xFF1E1B4B), Color(0xFF4F46E5), Color(0xFF06B6D4)),
likesYou = true
),
DatingProfile(
3, "Chloe", 25, 2,
"Film photographer who always has an extra 35mm roll and weekend roadtrip plans.",
listOf("35mm Film", "Roadtrips", "Indie Art"),
listOf(Color(0xFF064E3B), Color(0xFF10B981), Color(0xFFF59E0B)),
likesYou = false
),
DatingProfile(
4, "Noah", 29, 7,
"Golden retriever energy. Let's trade Spotify playlists and hit a Sunday farmers market.",
listOf("Live Music", "Cooking", "Dogs"),
listOf(Color(0xFF3B0764), Color(0xFF9333EA), Color(0xFFEC4899)),
likesYou = true
)
)
}
val deck = remember { mutableStateListOf(*initialDeck.toTypedArray()) }
val history = remember { mutableStateListOf<DatingProfile>() }
val matches = remember { mutableStateListOf<DatingProfile>() }
var recentMatch by remember { mutableStateOf<DatingProfile?>(null) }
var dragOffset by remember { mutableStateOf(Offset.Zero) }
fun handleSwipe(profile: DatingProfile, liked: Boolean) {
deck.remove(profile)
history.add(profile)
dragOffset = Offset.Zero
if (liked && profile.likesYou) {
if (!matches.any { it.id == profile.id }) {
matches.add(0, profile)
}
recentMatch = profile
}
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Top Header & Matches Counter
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text("VibeSpark π₯", fontSize = 22.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
Text("${deck.size} profiles nearby", fontSize = 12.sp, color = Color(0xFF94A3B8))
}
Surface(color = Color(0xFF1E293B), shape = RoundedCornerShape(20.dp)) {
Text(
text = "β₯ ${matches.size} Matches",
color = Color(0xFFF43F5E),
fontWeight = FontWeight.Bold,
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
)
}
}
if (matches.isNotEmpty()) {
Spacer(modifier = Modifier.height(10.dp))
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
items(matches, key = { it.id }) { m ->
Surface(color = Color(0xFF1E293B), shape = RoundedCornerShape(16.dp)) {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Box(
modifier = Modifier.size(22.dp).clip(CircleShape).background(Color(0xFFF43F5E)),
contentAlignment = Alignment.Center
) {
Text(m.name.take(1), fontSize = 11.sp, color = Color.White, fontWeight = FontWeight.Bold)
}
Text(m.name, fontSize = 12.sp, color = Color.White, fontWeight = FontWeight.SemiBold)
}
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// Swipe Card Stack Viewport
Box(
modifier = Modifier.weight(1f).fillMaxWidth(),
contentAlignment = Alignment.Center
) {
if (deck.isEmpty()) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("You've seen everyone nearby!", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.White)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
deck.clear()
deck.addAll(initialDeck)
history.clear()
}) {
Text("Reload Discovery Deck")
}
}
} else {
val topProfile = deck.first()
if (deck.size > 1) {
val nextProfile = deck[1]
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 14.dp, vertical = 10.dp)
.clip(RoundedCornerShape(26.dp))
.background(Brush.verticalGradient(nextProfile.gradientColors.map { it.copy(alpha = 0.45f) }))
)
}
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
translationX = dragOffset.x
translationY = dragOffset.y * 0.25f
rotationZ = (dragOffset.x / 28f).coerceIn(-18f, 18f)
}
.pointerInput(topProfile.id) {
detectDragGestures(
onDragEnd = {
when {
dragOffset.x > 220f -> handleSwipe(topProfile, liked = true)
dragOffset.x < -220f -> handleSwipe(topProfile, liked = false)
else -> dragOffset = Offset.Zero
}
},
onDrag = { change, dragAmount ->
change.consume()
dragOffset += dragAmount
}
)
}
.clip(RoundedCornerShape(26.dp))
.background(Brush.verticalGradient(topProfile.gradientColors))
.padding(22.dp)
) {
if (abs(dragOffset.x) > 40f) {
val isLike = dragOffset.x > 0
Box(
modifier = Modifier
.align(if (isLike) Alignment.TopStart else Alignment.TopEnd)
.border(3.dp, if (isLike) Color(0xFF4ADE80) else Color(0xFFF87171), RoundedCornerShape(10.dp))
.padding(horizontal = 12.dp, vertical = 4.dp)
) {
Text(
text = if (isLike) "LIKE" else "NOPE",
color = if (isLike) Color(0xFF4ADE80) else Color(0xFFF87171),
fontWeight = FontWeight.ExtraBold,
fontSize = 20.sp
)
}
}
Column(modifier = Modifier.align(Alignment.BottomStart)) {
Surface(color = Color.Black.copy(alpha = 0.35f), shape = RoundedCornerShape(12.dp)) {
Text(
text = "π ${topProfile.distanceKm} km away",
color = Color.White,
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "${topProfile.name}, ${topProfile.age}",
fontSize = 30.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.White
)
Spacer(modifier = Modifier.height(6.dp))
Text(topProfile.bio, fontSize = 14.sp, color = Color.White.copy(alpha = 0.92f), lineHeight = 20.sp)
Spacer(modifier = Modifier.height(12.dp))
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(topProfile.interests) { tag ->
Surface(color = Color.Black.copy(alpha = 0.4f), shape = RoundedCornerShape(20.dp)) {
Text(
text = tag,
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
)
}
}
}
}
}
}
}
Spacer(modifier = Modifier.height(14.dp))
// Bottom Tactile Action Dock
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
OutlinedButton(
enabled = history.isNotEmpty(),
onClick = {
if (history.isNotEmpty()) {
deck.add(0, history.removeLast())
dragOffset = Offset.Zero
}
},
shape = CircleShape
) {
Text("βΊ Rewind")
}
Button(
enabled = deck.isNotEmpty(),
onClick = { handleSwipe(deck.first(), liked = false) },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF334155)),
shape = CircleShape
) {
Text("β Pass", color = Color.White, fontWeight = FontWeight.Bold)
}
Button(
enabled = deck.isNotEmpty(),
onClick = { handleSwipe(deck.first(), liked = true) },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF43F5E)),
shape = CircleShape
) {
Text("β₯ Like", color = Color.White, fontWeight = FontWeight.Bold)
}
}
}
recentMatch?.let { matched ->
AlertDialog(
onDismissRequest = { recentMatch = null },
title = { Text("π It's a Match with ${matched.name}!", fontWeight = FontWeight.ExtraBold) },
text = {
Text("You and ${matched.name} both liked each other. Break the ice about ${matched.interests.first()}!")
},
confirmButton = {
Button(onClick = { recentMatch = null }) {
Text("Keep Swiping")
}
}
)
}
}
How to Debug Swipe Deck Glitches
Keep card transitions and drag gestures fluid with these three Compose fixes:
Next Card Inherits Old Drag Offset
Key your gesture modifier to the active profile ID and reset dragOffset = Offset.Zero on swipe:
Modifier.pointerInput(topProfile.id) { detectDragGestures(...) }
Card Snaps Back Abruptly
Animate the card back to center with spring physics when released below threshold: "Wrap dragOffset in animateOffsetAsState with spring(dampingRatio = Spring.DampingRatioMediumBouncy)."
Child Row Steals Horizontal Drag
If a scrollable row inside the card intercepts horizontal swipes, replace LazyRow with a non-scrolling Row or FlowRow for interest pills.
The Vibe Prompt Vault
Copy-paste prompts to add multi-photo story bars, discovery filter sheets, and icebreaker chat drawers.
πΈ Prompt: Add Multi-Photo Story Tap Bar Inside Each Profile Card
"Add a 3-segment photo/prompt carousel inside the top profile card in DatingSwipeScreen. Show 3 horizontal progress indicator bars at the top of the card, and let tapping the left or right side of the card step backward or forward through the profile's photo/prompt slides."
π§ Prompt: Add Discovery Filter Bottom Sheet (Distance & Age Range)
"Add a 'Filters' button in the top bar that opens a Material 3 ModalBottomSheet containing a maximum distance Slider (1km to 50km) and an age RangeSlider (18 to 45). Dynamically filter the swipe deck to only show profiles matching those criteria."
π¬ Prompt: Add Interactive Match Chat Drawer with Icebreaker Chips
"Make each matched profile pill in the top Matches LazyRow clickable. Tapping a match opens a full-height chat sheet displaying message bubbles, 3 clickable Icebreaker opener chips tailored to their interests, and a message input bar with simulated auto-replies."