Skip to main content

How to Make a Tinder-Like Dating App

Copied to clipboard!
No boilerplate memorization required

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 Philosophy

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

The 4-Step Blueprint

From a blank Android Studio project to a tactile swipe-and-match experience.

Step 01

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.

πŸ’‘ Pro Vibe Tip: No external card-stack library is requiredβ€”Modifier.graphicsLayer and detectDragGestures give you full control over card physics.
Step 02

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.

πŸ”‘ Key Constraint: Pass topProfile.id as the key to pointerInput(topProfile.id) so drag state resets cleanly when the top card changes.
Step 03

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.

✨ Iteration Secret: Render the second profile card slightly scaled down behind the active top card to create depth as the user drags.
Step 04

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.

πŸ† The Grand Payoff: A smooth, responsive matchmaking prototype ready to test on real devices in under half an hour.
Interactive Tool

Vibe Prompt Configurator

Select your dating app aesthetic, card physics engine, and matchmaking features. Copy the generated prompt directly into Claude, Cursor, or Gemini.

Special Feature Vibe
Optional custom details added straight into your master prompt.
Generated Master Prompt
Ready to paste into Claude, Gemini, or ChatGPT Single-file Compose ready
Product Spec

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.

UX Checklist Before Shipping: Keep horizontal card tilt smooth (rotationZ clamped between -18Β° and 18Β°), show clear LIKE/NOPE stamps during drag, and provide accessible Pass/Like buttons below the deck.
Starter Template

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.

app/src/main/java/.../MainActivity.kt
Kotlin + Compose
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")
                }
            }
        )
    }
}
Troubleshooting

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.

Extensions

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
Prompt Template

"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)
Prompt Template

"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
Prompt Template

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

We use cookies for ads and analytics.