Create an Android dating app: the complete manual

ddca91470330ebca32dc9e51b5be911f.png

Part 1: Android Dating App - Or, "How to Make Swiping Right Your Full-Time Job"

Welcome, future digital Cupid! So you want to build a dating app for Android? Excellent choice! You're about to create something that will consume more of people's time than their actual jobs. Let's build an app that's so addictive, users will forget they installed it to find love and just keep swiping for the dopamine hits!

Chapter 1.1: Setting Up Your Digital Love Laboratory

Step 1: Install the Required Weapons

Before we start coding like caffeinated monkeys, make sure you have:

  1. Android Studio - The magical IDE where dreams (and bugs) are born
  2. Java/Kotlin - We'll use Kotlin because it's sexier and more modern (like your future users)
  3. Android SDK - So your app actually runs on phones instead of just in your imagination
  4. An emulator or real device - Because testing on a microwave won't work

Step 2: Create a New Project - The Birth of Love

Open Android Studio and create a new project:

  1. Choose "Empty Activity" - because our users start empty-hearted too
  2. Name: "SwipeMaster 3000" (or something less ridiculous)
  3. Package name: com.yourname.datingapp (unless you want Google to reject it)
  4. Language: Kotlin
  5. Minimum SDK: API 21 (so even people with ancient phones can find love)

Step 3: Project Structure - Where the Magic Lives

Your project should look like this:

app/
├── src/
│   ├── main/
│   │   ├── java/com/yourname/datingapp/
│   │   ├── res/
│   │   │   ├── layout/
│   │   │   ├── values/
│   │   │   └── drawable/
│   │   └── AndroidManifest.xml
│   └── test/ (where broken dreams go to die)
├── build.gradle (the recipe for your love potion)
└── proguard-rules.pro (for when you want to hide your terrible code)

Chapter 1.2: Building the Foundation - Or, "Making It Look Pretty Before It Actually Works"

Step 1: Dependencies - The Spices in Your Love Stew

Open your app/build.gradle file and add these dependencies:

// app/build.gradle
dependencies {
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

    // For making network calls (to find love across the internet)
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'

    // For loading images (because profiles without pics are just creepy text)
    implementation 'com.github.bumptech.glide:glide:4.14.2'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'

    // For smooth animations (swiping should feel like butter, not sandpaper)
    implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'

    // For location services (to find love within 5km, not 5000km)
    implementation 'com.google.android.gms:play-services-location:21.0.1'

    // For Firebase (because Google wants to know your dating preferences too)
    implementation platform('com.google.firebase:firebase-bom:31.2.3')
    implementation 'com.google.firebase:firebase-auth-ktx'
    implementation 'com.google.firebase:firebase-firestore-ktx'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

Sync your project and pray to the coding gods that everything downloads without errors!

Step 2: Data Models - Because Love Needs Structure

Create a data package and add these model classes:

// data/User.kt
data class User(
    val id: String = "",
    val name: String = "",
    val age: Int = 0,
    val bio: String = "",
    val photoUrls: List<String> = emptyList(),
    val location: String = "",
    val gender: String = "",
    val lookingFor: String = "",
    val interests: List<String> = emptyList(),
    val lastActive: Date = Date(),
    val distance: Int = 0
) {
    // Helper method to get the first photo or a default
    fun getMainPhotoUrl(): String {
        return if (photoUrls.isNotEmpty()) photoUrls[0] else ""
    }

    // Because "25 years old" sounds better than just "25"
    fun getFormattedAge(): String = "$age years old"

    // For when the bio is emptier than their dating prospects
    fun getDisplayBio(): String {
        return if (bio.isNotEmpty()) bio else "Professional ghost, part-time human"
    }
}

// data/Match.kt
data class Match(
    val id: String = "",
    val user1Id: String = "",
    val user2Id: String = "",
    val matchedAt: Date = Date(),
    val lastMessage: String = "",
    val lastMessageTime: Date = Date(),
    val unreadCount: Int = 0
) {
    fun isUnread(): Boolean = unreadCount > 0
}

// data/Message.kt
data class Message(
    val id: String = "",
    val senderId: String = "",
    val receiverId: String = "",
    val content: String = "",
    val timestamp: Date = Date(),
    val isRead: Boolean = false,
    val messageType: MessageType = MessageType.TEXT
) {
    fun isSentByMe(currentUserId: String): Boolean {
        return senderId == currentUserId
    }
}

enum class MessageType {
    TEXT, IMAGE, LOCATION, GIF
}

// data/Swipe.kt - Because every swipe tells a story
data class Swipe(
    val swiperId: String = "",
    val swipedUserId: String = "",
    val isLiked: Boolean = false,
    val timestamp: Date = Date()
) {
    fun getSwipeType(): String = if (isLiked) "❤️" else "💔"
}

Step 3: The Main Activity - Where Lonely Hearts Unite

Let's create our main activity layout. Open activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- Our main swiping area -->
    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Bottom Navigation - Because every dating app needs one -->
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@android:color/white"
        app:menu="@menu/bottom_nav_menu"
        app:labelVisibilityMode="labeled"
        app:itemIconTint="@color/bottom_nav_colors"
        app:itemTextColor="@color/bottom_nav_colors" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Create the bottom navigation menu in res/menu/bottom_nav_menu.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/navigation_swipe"
        android:icon="@drawable/ic_swipe"
        android:title="Discover" />

    <item
        android:id="@+id/navigation_matches"
        android:icon="@drawable/ic_heart"
        android:title="Matches" />

    <item
        android:id="@+id/navigation_chat"
        android:icon="@drawable/ic_chat"
        android:title="Chat" />

    <item
        android:id="@+id/navigation_profile"
        android:icon="@drawable/ic_profile"
        android:title="Profile" />
</menu>

Create the color selector in res/color/bottom_nav_colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/purple_500" android:state_checked="true" />
    <item android:color="@color/gray" android:state_checked="false" />
</selector>

Step 4: MainActivity - The Brain of the Operation

Now let's code the MainActivity:

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val navController by lazy { findNavController(R.id.container) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupNavigation()
        checkAuthentication()

        // Log app open - because we're nosy
        Log.d("DatingApp", "Another lonely heart opened the app")
    }

    private fun setupNavigation() {
        // Connect BottomNavigationView with NavController
        binding.bottomNavigation.setupWithNavController(navController)

        // Listen for navigation changes
        navController.addOnDestinationChangedListener { _, destination, _ ->
            when (destination.id) {
                R.id.swipeFragment -> {
                    // User is desperately searching for love
                    Log.d("Navigation", "User is swiping through potential disappointments")
                }
                R.id.matchesFragment -> {
                    // User is checking their matches (probably zero)
                    Log.d("Navigation", "User is admiring their collection of matches")
                }
                R.id.chatFragment -> {
                    // User is trying to start conversations
                    Log.d("Navigation", "User is crafting pickup lines that will never get responses")
                }
                R.id.profileFragment -> {
                    // User is updating their fake profile
                    Log.d("Navigation", "User is pretending to be more interesting than they actually are")
                }
            }
        }
    }

    private fun checkAuthentication() {
        // Check if user is logged in
        if (!isUserLoggedIn()) {
            // Redirect to login screen
            navController.navigate(R.id.loginFragment)
            binding.bottomNavigation.visibility = View.GONE
        } else {
            binding.bottomNavigation.visibility = View.VISIBLE
        }
    }

    private fun isUserLoggedIn(): Boolean {
        // In reality, check SharedPreferences or Firebase Auth
        val sharedPref = getSharedPreferences("dating_app", Context.MODE_PRIVATE)
        return sharedPref.getBoolean("is_logged_in", false)
    }

    // Handle back button press
    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp() || super.onSupportNavigateUp()
    }

    // When user tries to exit but we guilt-trip them into staying
    override fun onBackPressed() {
        if (navController.currentDestination?.id == R.id.swipeFragment) {
            // Show exit confirmation dialog
            showExitConfirmation()
        } else {
            super.onBackPressed()
        }
    }

    private fun showExitConfirmation() {
        AlertDialog.Builder(this)
            .setTitle("Wait! Don't go!")
            .setMessage("Are you sure you want to leave? There might be someone perfect swiping right now!")
            .setPositiveButton("Stay and Swipe") { dialog, _ ->
                dialog.dismiss()
                // User decided to continue their desperate search
                Log.d("UserBehavior", "User succumbed to FOMO and continued swiping")
            }
            .setNegativeButton("Exit Anyway") { dialog, _ ->
                dialog.dismiss()
                finish()
            }
            .setCancelable(false)
            .show()
    }
}

Chapter 1.3: The Swiping Fragment - Where Magic (and Desperation) Happens

Step 1: Create the Swipe Card Layout

Create layout/fragment_swipe.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/light_gray"
    tools:context=".ui.swipe.SwipeFragment">

    <!-- Loading indicator for when we're fetching potential soulmates -->
    <ProgressBar
        android:id="@+id/loadingIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="visible" />

    <!-- Stack of profile cards -->
    <com.yuyakaido.android.cardstackview.CardStackView
        android:id="@+id/cardStackView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        app:cardstack_swipeable="true"
        app:cardstack_elevation="4"
        app:cardstack_scale="0.95" />

    <!-- Action buttons -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:orientation="horizontal"
        android:gravity="center"
        android:padding="24dp"
        android:layout_marginBottom="16dp">

        <!-- Dislike button -->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/dislikeButton"
            style="@style/Widget.Material3.Button.Icon"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:backgroundTint="@color/white"
            app:icon="@drawable/ic_close"
            app:iconTint="@color/red"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle" />

        <!-- Super like button (because regular like is for peasants) -->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/superLikeButton"
            style="@style/Widget.Material3.Button.Icon"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="32dp"
            android:layout_marginEnd="32dp"
            android:backgroundTint="@color/blue"
            app:icon="@drawable/ic_star"
            app:iconTint="@color/white"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle" />

        <!-- Like button -->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/likeButton"
            style="@style/Widget.Material3.Button.Icon"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:backgroundTint="@color/white"
            app:icon="@drawable/ic_heart"
            app:iconTint="@color/green"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle" />

    </LinearLayout>

    <!-- Out of profiles message (the saddest screen) -->
    <TextView
        android:id="@+id/emptyStateText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:padding="48dp"
        android:text="No more profiles in your area!\n\nTry expanding your search radius or moving to a more populated area. Or just accept your fate."
        android:textSize="18sp"
        android:textColor="@color/gray"
        android:visibility="gone" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Step 2: Create the Profile Card Layout

Create layout/item_profile_card.xml:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp"
    app:cardCornerRadius="16dp"
    app:cardElevation="8dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Profile Image with gradient overlay -->
        <ImageView
            android:id="@+id/profileImage"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:contentDescription="Profile image" />

        <!-- Gradient overlay for better text readability -->
        <View
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:layout_alignParentBottom="true"
            android:background="@drawable/gradient_overlay" />

        <!-- Profile Info -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:orientation="vertical"
            android:padding="24dp">

            <!-- Name and Age -->
            <TextView
                android:id="@+id/nameAgeText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Jessica, 28"
                android:textColor="@color/white"
                android:textSize="32sp"
                android:textStyle="bold"
                android:shadowColor="@color/black"
                android:shadowDx="2"
                android:shadowDy="2"
                android:shadowRadius="4" />

            <!-- Distance -->
            <TextView
                android:id="@+id/distanceText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="2 km away"
                android:textColor="@color/white"
                android:textSize="16sp"
                android:shadowColor="@color/black"
                android:shadowDx="1"
                android:shadowDy="1"
                android:shadowRadius="2" />

            <!-- Bio -->
            <TextView
                android:id="@+id/bioText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Professional dog petter, amateur human. Looking for someone who doesn't mind my obsession with true crime podcasts."
                android:textColor="@color/white"
                android:textSize="14sp"
                android:maxLines="3"
                android:ellipsize="end"
                android:shadowColor="@color/black"
                android:shadowDx="1"
                android:shadowDy="1"
                android:shadowRadius="2" />

            <!-- Interests -->
            <LinearLayout
                android:id="@+id/interestsLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:orientation="horizontal"
                android:visibility="gone">

                <!-- Interests will be added dynamically -->

            </LinearLayout>

        </LinearLayout>

        <!-- Photo counter -->
        <TextView
            android:id="@+id/photoCounter"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_alignParentEnd="true"
            android:layout_margin="16dp"
            android:background="@drawable/rounded_corner_dark"
            android:paddingHorizontal="12dp"
            android:paddingVertical="6dp"
            android:text="1/5"
            android:textColor="@color/white"
            android:textSize="12sp" />

    </RelativeLayout>

</com.google.android.material.card.MaterialCardView>

Create the gradient overlay in res/drawable/gradient_overlay.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:angle="90"
        android:startColor="@android:color/transparent"
        android:endColor="#80000000" />
</shape>

Step 3: The Swipe Fragment Code

Now for the main event! Create SwipeFragment.kt:

// ui/swipe/SwipeFragment.kt
class SwipeFragment : Fragment() {

    private var _binding: FragmentSwiipeBinding? = null
    private val binding get() = _binding!!

    private val viewModel: SwipeViewModel by viewModels()
    private lateinit var cardStackAdapter: ProfileCardAdapter

    // Track swipes to prevent spam
    private var lastSwipeTime = 0L
    private val swipeCooldown = 500L // milliseconds

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSwiipeBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupCardStack()
        setupClickListeners()
        observeViewModel()
        loadProfiles()

        Log.d("SwipeFragment", "User entered the meat market")
    }

    private fun setupCardStack() {
        cardStackAdapter = ProfileCardAdapter { user, position ->
            // Handle card click (show full profile)
            showProfileDetail(user)
        }

        binding.cardStackView.layoutManager = CardStackLayoutManager(requireContext(), object : CardStackListener {
            override fun onCardDragging(direction: Direction?, ratio: Float) {
                // Card is being dragged - show hint indicators
                updateSwipeIndicators(ratio, direction)
            }

            override fun onCardSwiped(direction: Direction?) {
                // Card was swiped - handle the swipe
                handleCardSwiped(direction)
            }

            override fun onCardRewound() {
                // User rewound a card (because they have commitment issues)
                Log.d("CardStack", "User can't make up their mind")
            }

            override fun onCardCanceled() {
                // Card returned to center (user got cold feet)
                hideSwipeIndicators()
            }

            override fun onCardAppeared(view: View?, position: Int) {
                // New card appeared
                val user = cardStackAdapter.getItem(position)
                Log.d("CardStack", "Now viewing: ${user.name}")
            }

            override fun onCardDisappeared(view: View?, position: Int) {
                // Card disappeared
            }
        })

        binding.cardStackView.adapter = cardStackAdapter
    }

    private fun setupClickListeners() {
        // Dislike button
        binding.dislikeButton.setOnClickListener {
            if (canSwipe()) {
                binding.cardStackView.swipeLeft()
                lastSwipeTime = System.currentTimeMillis()
            } else {
                showSwipeCooldownMessage()
            }
        }

        // Like button  
        binding.likeButton.setOnClickListener {
            if (canSwipe()) {
                binding.cardStackView.swipeRight()
                lastSwipeTime = System.currentTimeMillis()
            } else {
                showSwipeCooldownMessage()
            }
        }

        // Super like button
        binding.superLikeButton.setOnClickListener {
            if (canSwipe()) {
                // Super like animation
                performSuperLikeAnimation()
                binding.cardStackView.swipeRight() // Super like is just a fancy right swipe
                lastSwipeTime = System.currentTimeMillis()

                // Show super like confirmation
                showSuperLikeMessage()
            } else {
                showSwipeCooldownMessage()
            }
        }
    }

    private fun observeViewModel() {
        viewModel.profiles.observe(viewLifecycleOwner) { profiles ->
            when {
                profiles.isEmpty() -> showEmptyState()
                profiles.isNotEmpty() -> showProfiles(profiles)
            }
        }

        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
            binding.loadingIndicator.visibility = if (isLoading) View.VISIBLE else View.GONE
            binding.cardStackView.visibility = if (isLoading) View.GONE else View.VISIBLE
        }

        viewModel.error.observe(viewLifecycleOwner) { error ->
            error?.let {
                showError("Failed to load profiles: $it")
                Log.e("SwipeFragment", "Error loading profiles: $it")
            }
        }
    }

    private fun loadProfiles() {
        viewModel.loadProfiles()
    }

    private fun canSwipe(): Boolean {
        return System.currentTimeMillis() - lastSwipeTime > swipeCooldown
    }

    private fun showSwipeCooldownMessage() {
        Snackbar.make(binding.root, "Whoa there, slow down! Give each profile a chance.", Snackbar.LENGTH_SHORT).show()
    }

    private fun handleCardSwiped(direction: Direction?) {
        val swipedUser = cardStackAdapter.getCurrentItem() ?: return

        when (direction) {
            Direction.Right -> {
                // User liked the profile
                viewModel.likeUser(swipedUser)
                showLikeAnimation()
                Log.d("Swipe", "Liked: ${swipedUser.name}")
            }
            Direction.Left -> {
                // User disliked the profile
                viewModel.dislikeUser(swipedUser)
                showDislikeAnimation()
                Log.d("Swipe", "Disliked: ${swipedUser.name}")
            }
            else -> {
                // Other directions (up/down) - we don't care
                Log.d("Swipe", "Weird swipe direction: $direction")
            }
        }

        // Check if we need to load more profiles
        if (cardStackAdapter.itemCount < 3) {
            viewModel.loadMoreProfiles()
        }
    }

    private fun showLikeAnimation() {
        // Show a heart animation
        val heartView = ImageView(requireContext()).apply {
            setImageResource(R.drawable.ic_heart_filled)
            layoutParams = FrameLayout.LayoutParams(200, 200).apply {
                gravity = Gravity.CENTER
            }
        }

        (requireView() as ViewGroup).addView(heartView)

        heartView.animate()
            .scaleX(2f)
            .scaleY(2f)
            .alpha(0f)
            .setDuration(800)
            .withEndAction {
                (requireView() as ViewGroup).removeView(heartView)
            }
            .start()
    }

    private fun showDislikeAnimation() {
        // Show an X animation
        val xView = ImageView(requireContext()).apply {
            setImageResource(R.drawable.ic_close_filled)
            layoutParams = FrameLayout.LayoutParams(200, 200).apply {
                gravity = Gravity.CENTER
            }
        }

        (requireView() as ViewGroup).addView(xView)

        xView.animate()
            .scaleX(2f)
            .scaleY(2f)
            .alpha(0f)
            .setDuration(800)
            .withEndAction {
                (requireView() as ViewGroup).removeView(xView)
            }
            .start()
    }

    private fun performSuperLikeAnimation() {
        // Super like gets a special animation
        val superLikeView = ImageView(requireContext()).apply {
            setImageResource(R.drawable.ic_star_filled)
            layoutParams = FrameLayout.LayoutParams(250, 250).apply {
                gravity = Gravity.CENTER
            }
        }

        (requireView() as ViewGroup).addView(superLikeView)

        superLikeView.animate()
            .scaleX(3f)
            .scaleY(3f)
            .rotation(360f)
            .alpha(0f)
            .setDuration(1000)
            .withEndAction {
                (requireView() as ViewGroup).removeView(superLikeView)
            }
            .start()
    }

    private fun showSuperLikeMessage() {
        Snackbar.make(binding.root, "Super Like sent! You must really like them!", Snackbar.LENGTH_SHORT).show()
    }

    private fun updateSwipeIndicators(ratio: Float, direction: Direction?) {
        // Update UI based on swipe direction and intensity
        when (direction) {
            Direction.Right -> {
                // Show like indicator
                binding.likeButton.alpha = 0.5f + (ratio * 0.5f)
                binding.likeButton.scaleX = 1f + (ratio * 0.2f)
                binding.likeButton.scaleY = 1f + (ratio * 0.2f)
            }
            Direction.Left -> {
                // Show dislike indicator
                binding.dislikeButton.alpha = 0.5f + (ratio * 0.5f)
                binding.dislikeButton.scaleX = 1f + (ratio * 0.2f)
                binding.dislikeButton.scaleY = 1f + (ratio * 0.2f)
            }
            else -> {
                hideSwipeIndicators()
            }
        }
    }

    private fun hideSwipeIndicators() {
        binding.likeButton.animate().alpha(1f).scaleX(1f).scaleY(1f).start()
        binding.dislikeButton.animate().alpha(1f).scaleX(1f).scaleY(1f).start()
    }

    private fun showProfiles(profiles: List<User>) {
        cardStackAdapter.submitList(profiles)
        binding.emptyStateText.visibility = View.GONE
        binding.cardStackView.visibility = View.VISIBLE
    }

    private fun showEmptyState() {
        binding.emptyStateText.visibility = View.VISIBLE
        binding.cardStackView.visibility = View.GONE
        binding.loadingIndicator.visibility = View.GONE
    }

    private fun showProfileDetail(user: User) {
        // Navigate to profile detail screen
        findNavController().navigate(
            SwipeFragmentDirections.actionSwipeFragmentToProfileDetailFragment(user.id)
        )
    }

    private fun showError(message: String) {
        Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG)
            .setAction("Retry") { loadProfiles() }
            .show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        Log.d("SwipeFragment", "User left the meat market (probably to check their zero matches)")
    }
}

Step 4: Profile Card Adapter

Create ProfileCardAdapter.kt:

// ui/swipe/adapter/ProfileCardAdapter.kt
class ProfileCardAdapter(
    private val onItemClick: (User, Int) -> Unit
) : ListAdapter<User, ProfileCardAdapter.ProfileViewHolder>(UserDiffCallback()) {

    private var currentPosition = 0

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
        val binding = ItemProfileCardBinding.inflate(
            LayoutInflater.from(parent.context), 
            parent, 
            false
        )
        return ProfileViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
        val user = getItem(position)
        holder.bind(user)

        // Update current position for swipe tracking
        if (position == 0) {
            currentPosition = position
        }
    }

    fun getCurrentItem(): User? {
        return if (currentPosition < itemCount) getItem(currentPosition) else null
    }

    inner class ProfileViewHolder(
        private val binding: ItemProfileCardBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val user = getItem(position)
                    onItemClick(user, position)
                }
            }
        }

        fun bind(user: User) {
            binding.nameAgeText.text = "${user.name}, ${user.age}"
            binding.distanceText.text = "${user.distance} km away"
            binding.bioText.text = user.getDisplayBio()
            binding.photoCounter.text = "1/${user.photoUrls.size}"

            // Load profile image
            if (user.photoUrls.isNotEmpty()) {
                Glide.with(binding.root.context)
                    .load(user.getMainPhotoUrl())
                    .placeholder(R.drawable.ic_profile_placeholder)
                    .error(R.drawable.ic_profile_error)
                    .centerCrop()
                    .into(binding.profileImage)
            } else {
                binding.profileImage.setImageResource(R.drawable.ic_profile_placeholder)
            }

            // Load interests
            loadInterests(user.interests)
        }

        private fun loadInterests(interests: List<String>) {
            binding.interestsLayout.removeAllViews()

            if (interests.isEmpty()) {
                binding.interestsLayout.visibility = View.GONE
                return
            }

            binding.interestsLayout.visibility = View.VISIBLE

            // Add first 3 interests as chips
            interests.take(3).forEach { interest ->
                val chip = Chip(binding.root.context).apply {
                    text = interest
                    isClickable = false
                    setChipBackgroundColorResource(R.color.chip_background)
                    setTextColor(ContextCompat.getColor(context, R.color.white))
                    chipStrokeWidth = 0f
                }
                binding.interestsLayout.addView(chip)
            }

            // Show "+X more" if there are more interests
            if (interests.size > 3) {
                val moreChip = Chip(binding.root.context).apply {
                    text = "+${interests.size - 3} more"
                    isClickable = false
                    setChipBackgroundColorResource(R.color.chip_background)
                    setTextColor(ContextCompat.getColor(context, R.color.white))
                    chipStrokeWidth = 0f
                }
                binding.interestsLayout.addView(moreChip)
            }
        }
    }

    class UserDiffCallback : DiffUtil.ItemCallback<User>() {
        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem == newItem
        }
    }
}

Chapter 1.4: ViewModel and Repository - The Brains Behind the Beauty

Step 1: Swipe ViewModel

Create SwipeViewModel.kt:

// ui/swipe/SwipeViewModel.kt
@HiltViewModel
class SwipeViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _profiles = MutableLiveData<List<User>>()
    val profiles: LiveData<List<User>> = _profiles

    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading

    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error

    private var currentPage = 0
    private val pageSize = 20

    init {
        Log.d("SwipeViewModel", "SwipeViewModel initialized - ready to judge people based on photos")
    }

    fun loadProfiles() {
        _isLoading.value = true
        _error.value = null

        viewModelScope.launch {
            try {
                val newProfiles = userRepository.getProfiles(pageSize, currentPage)
                _profiles.value = newProfiles
                currentPage++

                if (newProfiles.isEmpty()) {
                    _error.value = "No profiles found in your area. Time to move?"
                }

                Log.d("SwipeViewModel", "Loaded ${newProfiles.size} profiles")
            } catch (e: Exception) {
                _error.value = e.message
                Log.e("SwipeViewModel", "Error loading profiles", e)
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun loadMoreProfiles() {
        if (_isLoading.value == true) return

        viewModelScope.launch {
            try {
                val moreProfiles = userRepository.getProfiles(pageSize, currentPage)
                val currentProfiles = _profiles.value ?: emptyList()
                _profiles.value = currentProfiles + moreProfiles
                currentPage++

                Log.d("SwipeViewModel", "Loaded ${moreProfiles.size} more profiles. Total: ${_profiles.value?.size}")
            } catch (e: Exception) {
                Log.e("SwipeViewModel", "Error loading more profiles", e)
                // Don't show error for pagination failures
            }
        }
    }

    fun likeUser(user: User) {
        viewModelScope.launch {
            try {
                userRepository.likeUser(user.id)
                Log.d("SwipeViewModel", "Liked user: ${user.name}")

                // Check for match
                checkForMatch(user.id)
            } catch (e: Exception) {
                Log.e("SwipeViewModel", "Error liking user", e)
            }
        }
    }

    fun dislikeUser(user: User) {
        viewModelScope.launch {
            try {
                userRepository.dislikeUser(user.id)
                Log.d("SwipeViewModel", "Disliked user: ${user.name}")
            } catch (e: Exception) {
                Log.e("SwipeViewModel", "Error disliking user", e)
            }
        }
    }

    private suspend fun checkForMatch(likedUserId: String) {
        try {
            val isMatch = userRepository.checkMatch(likedUserId)
            if (isMatch) {
                // Show match notification
                Log.d("SwipeViewModel", "IT'S A MATCH! 🎉")
                // In reality, you'd trigger a notification here
            }
        } catch (e: Exception) {
            Log.e("SwipeViewModel", "Error checking for match", e)
        }
    }

    fun clearError() {
        _error.value = null
    }
}

Step 2: User Repository

Create data/repository/UserRepository.kt:

// data/repository/UserRepository.kt
class UserRepository @Inject constructor(
    private val userService: UserService,
    private val preferences: SharedPreferences
) {

    suspend fun getProfiles(limit: Int, page: Int): List<User> {
        return try {
            // In reality, this would be an API call
            // For now, we'll generate mock data
            generateMockProfiles(limit)
        } catch (e: Exception) {
            Log.e("UserRepository", "Error getting profiles", e)
            emptyList()
        }
    }

    suspend fun likeUser(userId: String): Boolean {
        return try {
            // Simulate API call
            delay(100) // Simulate network delay
            Log.d("UserRepository", "Liked user: $userId")
            true
        } catch (e: Exception) {
            Log.e("UserRepository", "Error liking user", e)
            false
        }
    }

    suspend fun dislikeUser(userId: String): Boolean {
        return try {
            // Simulate API call
            delay(100) // Simulate network delay
            Log.d("UserRepository", "Disliked user: $userId")
            true
        } catch (e: Exception) {
            Log.e("UserRepository", "Error disliking user", e)
            false
        }
    }

    suspend fun checkMatch(userId: String): Boolean {
        return try {
            // Simulate API call to check if it's a match
            delay(150)
            // 10% chance of match for demo purposes
            Random.nextBoolean() && Random.nextDouble() < 0.1
        } catch (e: Exception) {
            Log.e("UserRepository", "Error checking match", e)
            false
        }
    }

    private fun generateMockProfiles(count: Int): List<User> {
        val names = listOf(
            "Emma", "Liam", "Olivia", "Noah", "Ava", "Oliver", 
            "Sophia", "Elijah", "Charlotte", "William", "Amelia", "James"
        )

        val bios = listOf(
            "Professional dog petter, amateur human. Looking for someone who doesn't mind my obsession with true crime podcasts.",
            "I can cook a mean pasta and tell worse dad jokes. Swipe right if you appreciate both.",
            "Just a simple person looking for someone to share memes and silence with.",
            "I put the 'pro' in procrastination. Also good at cuddling and eating pizza.",
            "Looking for someone to explore the world with. Or just explore the local coffee shops. Either works.",
            "I'm like a pizza - if you don't like me, there's probably something wrong with you.",
            "Professional overthinker, part-time human. Let's be awkward together.",
            "I'm not saying I'm Batman, but have you ever seen me and Batman in the same room?",
            "Looking for someone to share my snacks with. Must be okay with stolen fries.",
            "I'm the human equivalent of a warm blanket and a good book."
        )

        val interests = listOf(
            "Hiking", "Cooking", "Movies", "Travel", "Music", "Gaming",
            "Reading", "Sports", "Art", "Photography", "Dancing", "Yoga"
        )

        return List(count) { index ->
            User(
                id = "user_${System.currentTimeMillis()}_$index",
                name = names.random(),
                age = (22..35).random(),
                bio = bios.random(),
                photoUrls = listOf(
                    "https://picsum.photos/400/600?random=${System.currentTimeMillis() + index}",
                    "https://picsum.photos/400/600?random=${System.currentTimeMillis() + index + 1}",
                    "https://picsum.photos/400/600?random=${System.currentTimeMillis() + index + 2}"
                ),
                location = "New York, NY",
                gender = if (Random.nextBoolean()) "Female" else "Male",
                lookingFor = "Relationship",
                interests = interests.shuffled().take(5),
                distance = (1..20).random()
            )
        }
    }
}

What We've Built So Far - The Digital Meat Market is Open!

🎉 CONGRATULATIONS! You've just built the foundation of an Android dating app that's already more functional than most people's love lives!

What We've Accomplished:

  1. Project Setup: Created a modern Android app with all the necessary dependencies
  2. Data Models: Built the structure for users, matches, and messages
  3. Main Architecture: Set up navigation and basic app structure
  4. Swipe Interface: Created a Tinder-like swiping experience with smooth animations
  5. Profile Cards: Beautiful cards showing user information with images and interests
  6. ViewModel & Repository: Proper architecture for data management
  7. Mock Data: Generated realistic fake profiles for testing

Key Features Working:

Joke Break - Because Coding Should Be Fun:

Why did the Android developer break up with his girlfriend? She kept returning his intents!

What's a programmer's favorite dating app feature? The one that automatically filters out people who use light mode!

Why was the dating app developer always single? He was too busy fixing other people's relationships!

Your app now has the core swiping functionality that made dating apps famous (or infamous). Users can:

In the next part, we'll build the matches screen, chat functionality, user profiles, and actually connect to a real backend! But for now, pat yourself on the back - you've built the digital equivalent of a crowded bar where everyone's judging each other based on photos! 🚀💕

Next up: Matches, Chat, and making this thing actually useful!

Part 2: Android Dating App - Or, "From Swiping to Actually Talking (The Scary Part)"

Welcome back, you digital matchmaking maestro! We've built the swiping functionality, but let's face it - swiping is the easy part. Now we need to handle what happens when people actually match and have to... gulp... talk to each other. Let's build the features that separate the players from the "hey" senders!

Chapter 2.1: The Matches Fragment - "Where Hope Meets Reality"

Step 1: Matches Layout - The Trophy Case of Digital Romance

Create layout/fragment_matches.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.matches.MatchesFragment">

    <!-- App Bar -->
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.SwipeMaster3000.AppBarOverlay">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@android:color/white"
            app:title="Matches"
            app:titleTextColor="@color/black"
            app:menu="@menu/matches_menu" />

    </com.google.android.material.appbar.AppBarLayout>

    <!-- Main Content -->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <!-- New Matches Section -->
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="24dp"
                android:layout_marginTop="24dp"
                android:layout_marginEnd="24dp"
                android:text="New Matches"
                android:textSize="20sp"
                android:textStyle="bold"
                android:textColor="@color/black" />

            <!-- Horizontal RecyclerView for new matches -->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/newMatchesRecyclerView"
                android:layout_width="match_parent"
                android:layout_height="140dp"
                android:layout_marginTop="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginEnd="16dp"
                android:orientation="horizontal"
                tools:listitem="@layout/item_new_match" />

            <!-- Messages Section -->
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="24dp"
                android:layout_marginTop="32dp"
                android:layout_marginEnd="24dp"
                android:text="Messages"
                android:textSize="20sp"
                android:textStyle="bold"
                android:textColor="@color/black" />

            <!-- Messages List -->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/messagesRecyclerView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginEnd="16dp"
                android:orientation="vertical"
                tools:listitem="@layout/item_match_message" />

            <!-- Empty State -->
            <LinearLayout
                android:id="@+id/emptyState"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="48dp"
                android:orientation="vertical"
                android:gravity="center"
                android:visibility="gone">

                <ImageView
                    android:layout_width="120dp"
                    android:layout_height="120dp"
                    android:src="@drawable/ic_empty_matches"
                    android:contentDescription="No matches" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="24dp"
                    android:text="No Matches Yet"
                    android:textSize="24sp"
                    android:textStyle="bold"
                    android:textColor="@color/black" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:gravity="center"
                    android:text="Keep swiping to find your perfect match! Or maybe lower your standards a bit."
                    android:textSize="16sp"
                    android:textColor="@color/gray" />

                <com.google.android.material.button.MaterialButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="24dp"
                    android:text="Start Swiping"
                    android:textColor="@color/white"
                    app:icon="@drawable/ic_swipe"
                    app:iconTint="@color/white"
                    app:backgroundTint="@color/purple_500" />

            </LinearLayout>

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

    <!-- Loading Indicator -->
    <ProgressBar
        android:id="@+id/loadingIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Step 2: New Match Item Layout

Create layout/item_new_match.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="100dp"
    android:layout_height="130dp"
    android:layout_margin="8dp"
    app:cardCornerRadius="12dp"
    app:cardElevation="4dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Profile Image -->
        <ImageView
            android:id="@+id/profileImage"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:scaleType="centerCrop"
            android:contentDescription="Profile image" />

        <!-- New Match Badge -->
        <TextView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_alignParentTop="true"
            android:layout_alignParentEnd="true"
            android:layout_margin="4dp"
            android:background="@drawable/badge_new_match"
            android:gravity="center"
            android:text="NEW"
            android:textColor="@color/white"
            android:textSize="8sp"
            android:textStyle="bold" />

        <!-- User Name -->
        <TextView
            android:id="@+id/userName"
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:background="@color/white"
            android:gravity="center"
            android:padding="4dp"
            android:text="Emma"
            android:textColor="@color/black"
            android:textSize="12sp"
            android:textStyle="bold"
            android:maxLines="1"
            android:ellipsize="end" />

    </RelativeLayout>

</androidx.cardview.widget.CardView>

Step 3: Match Message Item Layout

Create layout/item_match_message.xml:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="12dp"
    app:cardElevation="2dp"
    android:clickable="true"
    android:focusable="true"
    android:foreground="?attr/selectableItemBackground">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp">

        <!-- Profile Image -->
        <ImageView
            android:id="@+id/profileImage"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_centerVertical="true"
            android:scaleType="centerCrop"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle"
            android:contentDescription="Profile image" />

        <!-- Unread Badge -->
        <TextView
            android:id="@+id/unreadBadge"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_alignStart="@id/profileImage"
            android:layout_alignTop="@id/profileImage"
            android:layout_marginStart="-4dp"
            android:layout_marginTop="-4dp"
            android:background="@drawable/badge_unread"
            android:gravity="center"
            android:text="3"
            android:textColor="@color/white"
            android:textSize="10sp"
            android:textStyle="bold"
            android:visibility="gone" />

        <!-- Text Content -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toEndOf="@id/profileImage"
            android:layout_centerVertical="true"
            android:layout_marginStart="16dp"
            android:layout_toStartOf="@id/timeText"
            android:orientation="vertical">

            <TextView
                android:id="@+id/userName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Emma"
                android:textColor="@color/black"
                android:textSize="16sp"
                android:textStyle="bold"
                android:maxLines="1"
                android:ellipsize="end" />

            <TextView
                android:id="@+id/lastMessage"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="Hey! How's your day going?"
                android:textColor="@color/gray"
                android:textSize="14sp"
                android:maxLines="1"
                android:ellipsize="end" />

        </LinearLayout>

        <!-- Time and Status -->
        <LinearLayout
            android:id="@+id/timeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:orientation="vertical"
            android:gravity="end">

            <TextView
                android:id="@+id/time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="2h ago"
                android:textColor="@color/gray"
                android:textSize="12sp" />

            <!-- Read Status -->
            <ImageView
                android:id="@+id/readStatus"
                android:layout_width="16dp"
                android:layout_height="16dp"
                android:layout_marginTop="4dp"
                android:src="@drawable/ic_double_tick"
                android:contentDescription="Read status"
                android:visibility="gone" />

        </LinearLayout>

    </RelativeLayout>

</com.google.android.material.card.MaterialCardView>

Step 4: Matches Fragment Code

Now let's create the MatchesFragment:

// ui/matches/MatchesFragment.kt
@AndroidEntryPoint
class MatchesFragment : Fragment() {

    private var _binding: FragmentMatchesBinding? = null
    private val binding get() = _binding!!

    private val viewModel: MatchesViewModel by viewModels()

    private lateinit var newMatchesAdapter: NewMatchesAdapter
    private lateinit var messagesAdapter: MessagesAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentMatchesBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupToolbar()
        setupAdapters()
        setupObservers()
        loadMatches()

        Log.d("MatchesFragment", "User is checking their collection of matches (probably empty)")
    }

    private fun setupToolbar() {
        binding.toolbar.setOnMenuItemClickListener { menuItem ->
            when (menuItem.itemId) {
                R.id.action_search -> {
                    // Search through matches
                    showSearchDialog()
                    true
                }
                R.id.action_filter -> {
                    // Filter matches
                    showFilterDialog()
                    true
                }
                else -> false
            }
        }
    }

    private fun setupAdapters() {
        // New Matches Adapter (Horizontal)
        newMatchesAdapter = NewMatchesAdapter { match ->
            openChat(match)
        }

        binding.newMatchesRecyclerView.apply {
            adapter = newMatchesAdapter
            layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
            addItemDecoration(SpacingItemDecoration(8))
        }

        // Messages Adapter (Vertical)
        messagesAdapter = MessagesAdapter { match ->
            openChat(match)
        }

        binding.messagesRecyclerView.apply {
            adapter = messagesAdapter
            layoutManager = LinearLayoutManager(requireContext())
        }

        // Set empty state click listener
        binding.emptyState.getChildAt(3).setOnClickListener {
            // Navigate back to swipe fragment
            findNavController().navigate(R.id.swipeFragment)
        }
    }

    private fun setupObservers() {
        viewModel.newMatches.observe(viewLifecycleOwner) { matches ->
            newMatchesAdapter.submitList(matches)
            updateNewMatchesVisibility(matches.isNotEmpty())
        }

        viewModel.messageMatches.observe(viewLifecycleOwner) { matches ->
            messagesAdapter.submitList(matches)
            updateMessagesVisibility(matches.isNotEmpty())
        }

        viewModel.isEmpty.observe(viewLifecycleOwner) { isEmpty ->
            binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE
        }

        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
            binding.loadingIndicator.visibility = if (isLoading) View.VISIBLE else View.GONE
        }

        viewModel.error.observe(viewLifecycleOwner) { error ->
            error?.let {
                showError(it)
                Log.e("MatchesFragment", "Error loading matches: $it")
            }
        }
    }

    private fun loadMatches() {
        viewModel.loadMatches()
    }

    private fun updateNewMatchesVisibility(hasNewMatches: Boolean) {
        val newMatchesTitle = binding.root.findViewById<TextView>(R.id.textView3) // You'd use proper ID in real app
        newMatchesTitle.visibility = if (hasNewMatches) View.VISIBLE else View.GONE
        binding.newMatchesRecyclerView.visibility = if (hasNewMatches) View.VISIBLE else View.GONE
    }

    private fun updateMessagesVisibility(hasMessages: Boolean) {
        val messagesTitle = binding.root.findViewById<TextView>(R.id.textView4) // You'd use proper ID in real app
        messagesTitle.visibility = if (hasMessages) View.VISIBLE else View.GONE
        binding.messagesRecyclerView.visibility = if (hasMessages) View.VISIBLE else View.GONE
    }

    private fun openChat(match: Match) {
        // Navigate to chat screen
        findNavController().navigate(
            MatchesFragmentDirections.actionMatchesFragmentToChatFragment(
                matchId = match.id,
                otherUserId = if (match.user1Id == getCurrentUserId()) match.user2Id else match.user1Id
            )
        )

        Log.d("MatchesFragment", "Opening chat with match: ${match.id}")
    }

    private fun showSearchDialog() {
        val dialog = MaterialAlertDialogBuilder(requireContext())
            .setTitle("Search Matches")
            .setMessage("This is where you'd search through your matches. But let's be honest, you don't have that many.")
            .setPositiveButton("Search Anyway") { dialog, _ ->
                // Implement search
                Snackbar.make(binding.root, "Searching... just kidding!", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .create()

        dialog.show()
    }

    private fun showFilterDialog() {
        val filters = arrayOf("Active Recently", "Unread Messages", "With Photos", "Super Likes")
        val checkedItems = booleanArrayOf(true, false, true, false)

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Filter Matches")
            .setMultiChoiceItems(filters, checkedItems) { _, which, isChecked ->
                // Handle filter selection
                checkedItems[which] = isChecked
                Log.d("MatchesFragment", "Filter ${filters[which]} set to $isChecked")
            }
            .setPositiveButton("Apply") { dialog, _ ->
                viewModel.applyFilters(checkedItems, filters)
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun showError(message: String) {
        Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG)
            .setAction("Retry") { loadMatches() }
            .show()
    }

    private fun getCurrentUserId(): String {
        // In reality, get from shared preferences or Firebase Auth
        return "current_user_id"
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        Log.d("MatchesFragment", "User left matches screen (probably disappointed)")
    }
}

// Item decoration for spacing
class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.left = spacing / 2
        outRect.right = spacing / 2
    }
}

Step 5: Matches Adapters

Create the adapters for our matches:

// ui/matches/adapter/NewMatchesAdapter.kt
class NewMatchesAdapter(
    private val onItemClick: (Match) -> Unit
) : ListAdapter<Match, NewMatchesAdapter.NewMatchViewHolder>(MatchDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewMatchViewHolder {
        val binding = ItemNewMatchBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return NewMatchViewHolder(binding)
    }

    override fun onBindViewHolder(holder: NewMatchViewHolder, position: Int) {
        val match = getItem(position)
        holder.bind(match)
    }

    inner class NewMatchViewHolder(
        private val binding: ItemNewMatchBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val match = getItem(position)
                    onItemClick(match)
                }
            }
        }

        fun bind(match: Match) {
            // For demo, we'll use a placeholder. In reality, fetch user data
            binding.userName.text = "User ${match.id.hashCode() % 1000}"

            // Load profile image (using random picsum for demo)
            Glide.with(binding.root.context)
                .load("https://picsum.photos/200/300?random=${match.id.hashCode()}")
                .placeholder(R.drawable.ic_profile_placeholder)
                .error(R.drawable.ic_profile_error)
                .centerCrop()
                .into(binding.profileImage)
        }
    }
}

// ui/matches/adapter/MessagesAdapter.kt
class MessagesAdapter(
    private val onItemClick: (Match) -> Unit
) : ListAdapter<Match, MessagesAdapter.MessageViewHolder>(MatchDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
        val binding = ItemMatchMessageBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return MessageViewHolder(binding)
    }

    override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
        val match = getItem(position)
        holder.bind(match)
    }

    inner class MessageViewHolder(
        private val binding: ItemMatchMessageBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val match = getItem(position)
                    onItemClick(match)
                }
            }
        }

        fun bind(match: Match) {
            binding.userName.text = "User ${match.id.hashCode() % 1000}"
            binding.lastMessage.text = match.lastMessage.ifEmpty { "Start a conversation!" }
            binding.time.text = getRelativeTime(match.lastMessageTime)

            // Show unread badge if there are unread messages
            if (match.unreadCount > 0) {
                binding.unreadBadge.visibility = View.VISIBLE
                binding.unreadBadge.text = match.unreadCount.toString()
                binding.lastMessage.setTextColor(ContextCompat.getColor(binding.root.context, R.color.black))
                binding.lastMessage.setTypeface(null, Typeface.BOLD)
            } else {
                binding.unreadBadge.visibility = View.GONE
                binding.lastMessage.setTextColor(ContextCompat.getColor(binding.root.context, R.color.gray))
                binding.lastMessage.setTypeface(null, Typeface.NORMAL)
            }

            // Show read status for sent messages
            binding.readStatus.visibility = if (match.lastMessage.isNotEmpty() && match.unreadCount == 0) {
                View.VISIBLE
            } else {
                View.GONE
            }

            // Load profile image
            Glide.with(binding.root.context)
                .load("https://picsum.photos/200/300?random=${match.id.hashCode()}")
                .placeholder(R.drawable.ic_profile_placeholder)
                .error(R.drawable.ic_profile_error)
                .centerCrop()
                .circleCrop()
                .into(binding.profileImage)
        }

        private fun getRelativeTime(date: Date): String {
            val now = Date()
            val diff = now.time - date.time
            val minutes = diff / (60 * 1000)
            val hours = diff / (60 * 60 * 1000)
            val days = diff / (24 * 60 * 60 * 1000)

            return when {
                minutes < 1 -> "Just now"
                minutes < 60 -> "${minutes.toInt()}m ago"
                hours < 24 -> "${hours.toInt()}h ago"
                days < 7 -> "${days.toInt()}d ago"
                else -> SimpleDateFormat("MMM dd", Locale.getDefault()).format(date)
            }
        }
    }
}

class MatchDiffCallback : DiffUtil.ItemCallback<Match>() {
    override fun areItemsTheSame(oldItem: Match, newItem: Match): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Match, newItem: Match): Boolean {
        return oldItem == newItem
    }
}

Step 6: Matches ViewModel

Create the ViewModel for matches:

// ui/matches/MatchesViewModel.kt
@HiltViewModel
class MatchesViewModel @Inject constructor(
    private val matchesRepository: MatchesRepository
) : ViewModel() {

    private val _newMatches = MutableLiveData<List<Match>>()
    val newMatches: LiveData<List<Match>> = _newMatches

    private val _messageMatches = MutableLiveData<List<Match>>()
    val messageMatches: LiveData<List<Match>> = _messageMatches

    private val _isEmpty = MutableLiveData<Boolean>()
    val isEmpty: LiveData<Boolean> = _isEmpty

    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading

    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error

    init {
        Log.d("MatchesViewModel", "MatchesViewModel initialized - ready to handle user's disappointment")
    }

    fun loadMatches() {
        _isLoading.value = true
        _error.value = null

        viewModelScope.launch {
            try {
                val allMatches = matchesRepository.getMatches()

                // Separate new matches (no messages yet) from matches with messages
                val newMatchesList = allMatches.filter { it.lastMessage.isEmpty() }
                val messageMatchesList = allMatches.filter { it.lastMessage.isNotEmpty() }

                _newMatches.value = newMatchesList
                _messageMatches.value = messageMatchesList
                _isEmpty.value = allMatches.isEmpty()

                Log.d("MatchesViewModel", "Loaded ${allMatches.size} matches (${newMatchesList.size} new, ${messageMatchesList.size} with messages)")
            } catch (e: Exception) {
                _error.value = "Failed to load matches: ${e.message}"
                Log.e("MatchesViewModel", "Error loading matches", e)
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun applyFilters(checkedItems: BooleanArray, filters: Array<String>) {
        viewModelScope.launch {
            try {
                // In reality, you'd apply these filters to the data
                val filteredMatches = matchesRepository.getFilteredMatches(checkedItems, filters)

                val newMatchesList = filteredMatches.filter { it.lastMessage.isEmpty() }
                val messageMatchesList = filteredMatches.filter { it.lastMessage.isNotEmpty() }

                _newMatches.value = newMatchesList
                _messageMatches.value = messageMatchesList
                _isEmpty.value = filteredMatches.isEmpty()

                Log.d("MatchesViewModel", "Applied filters, showing ${filteredMatches.size} matches")
            } catch (e: Exception) {
                _error.value = "Failed to apply filters: ${e.message}"
                Log.e("MatchesViewModel", "Error applying filters", e)
            }
        }
    }

    fun markAsRead(matchId: String) {
        viewModelScope.launch {
            try {
                matchesRepository.markAsRead(matchId)
                // Reload matches to update UI
                loadMatches()
                Log.d("MatchesViewModel", "Marked match $matchId as read")
            } catch (e: Exception) {
                Log.e("MatchesViewModel", "Error marking match as read", e)
            }
        }
    }
}

Chapter 2.2: The Chat Fragment - "Where Conversations Go to Die"

Step 1: Chat Layout - Where Magic (and Awkwardness) Happens

Create layout/fragment_chat.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.chat.ChatFragment">

    <!-- App Bar -->
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.SwipeMaster3000.AppBarOverlay">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@android:color/white">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingStart="8dp"
                android:paddingEnd="16dp">

                <!-- Back Button -->
                <ImageButton
                    android:id="@+id/backButton"
                    android:layout_width="48dp"
                    android:layout_height="48dp"
                    android:layout_centerVertical="true"
                    android:background="?attr/selectableItemBackgroundBorderless"
                    android:src="@drawable/ic_arrow_back"
                    android:contentDescription="Back"
                    app:tint="@color/black" />

                <!-- User Info -->
                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:layout_toStartOf="@+id/menuButton"
                    android:layout_toEndOf="@id/backButton"
                    android:gravity="center"
                    android:orientation="horizontal">

                    <ImageView
                        android:id="@+id/profileImage"
                        android:layout_width="36dp"
                        android:layout_height="36dp"
                        android:scaleType="centerCrop"
                        app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle"
                        android:contentDescription="Profile image" />

                    <LinearLayout
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="12dp"
                        android:orientation="vertical">

                        <TextView
                            android:id="@+id/userName"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="Emma"
                            android:textColor="@color/black"
                            android:textSize="16sp"
                            android:textStyle="bold"
                            android:maxLines="1"
                            android:ellipsize="end" />

                        <TextView
                            android:id="@+id/onlineStatus"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="Online"
                            android:textColor="@color/green"
                            android:textSize="12sp" />

                    </LinearLayout>

                </LinearLayout>

                <!-- Menu Button -->
                <ImageButton
                    android:id="@+id/menuButton"
                    android:layout_width="48dp"
                    android:layout_height="48dp"
                    android:layout_alignParentEnd="true"
                    android:layout_centerVertical="true"
                    android:background="?attr/selectableItemBackgroundBorderless"
                    android:src="@drawable/ic_more_vert"
                    android:contentDescription="Menu"
                    app:tint="@color/black" />

            </RelativeLayout>

        </com.google.android.material.appbar.MaterialToolbar>

    </com.google.android.material.appbar.AppBarLayout>

    <!-- Messages List -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/messagesRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@color/chat_background"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:listitem="@layout/item_message" />

    <!-- Message Input -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@android:color/white"
        android:orientation="horizontal"
        android:padding="16dp">

        <!-- Attachment Button -->
        <ImageButton
            android:id="@+id/attachmentButton"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:src="@drawable/ic_attachment"
            android:contentDescription="Attach file"
            app:tint="@color/gray" />

        <!-- Message Input -->
        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            app:boxBackgroundMode="outline"
            app:boxCornerRadiusBottomEnd="24dp"
            app:boxCornerRadiusBottomStart="24dp"
            app:boxCornerRadiusTopEnd="24dp"
            app:boxCornerRadiusTopStart="24dp"
            app:boxStrokeColor="@color/gray">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/messageInput"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Type a message..."
                android:maxLines="5"
                android:inputType="textMultiLine"
                android:background="@android:color/transparent" />

        </com.google.android.material.textfield.TextInputLayout>

        <!-- Send Button -->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/sendButton"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:backgroundTint="@color/purple_500"
            app:icon="@drawable/ic_send"
            app:iconTint="@color/white"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle" />

    </LinearLayout>

    <!-- Typing Indicator -->
    <LinearLayout
        android:id="@+id/typingIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|start"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="80dp"
        android:background="@drawable/typing_indicator_background"
        android:orientation="horizontal"
        android:padding="12dp"
        android:visibility="gone">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Emma is typing"
            android:textColor="@color/gray"
            android:textSize="12sp" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:orientation="horizontal">

            <View
                android:layout_width="4dp"
                android:layout_height="4dp"
                android:background="@color/gray"
                android:layout_marginEnd="2dp"
                android:alpha="0.4" />

            <View
                android:layout_width="4dp"
                android:layout_height="4dp"
                android:background="@color/gray"
                android:layout_marginEnd="2dp"
                android:alpha="0.7" />

            <View
                android:layout_width="4dp"
                android:layout_height="4dp"
                android:background="@color/gray" />

        </LinearLayout>

    </LinearLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Step 2: Message Item Layouts

Create sent and received message layouts:

<!-- layout/item_message_sent.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="end"
    android:padding="8dp">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="end">

        <com.google.android.material.card.MaterialCardView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="60dp"
            app:cardBackgroundColor="@color/purple_500"
            app:cardCornerRadius="16dp"
            app:cardElevation="2dp">

            <TextView
                android:id="@+id/messageText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="12dp"
                android:text="This is a sent message"
                android:textColor="@color/white"
                android:textSize="16sp" />

        </com.google.android.material.card.MaterialCardView>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="end"
            android:padding="4dp">

            <TextView
                android:id="@+id/timeText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="10:30 AM"
                android:textColor="@color/gray"
                android:textSize="12sp" />

            <ImageView
                android:id="@+id/readStatus"
                android:layout_width="16dp"
                android:layout_height="16dp"
                android:layout_marginStart="4dp"
                android:src="@drawable/ic_double_tick"
                android:contentDescription="Read status"
                app:tint="@color/purple_500" />

        </LinearLayout>

    </LinearLayout>

</LinearLayout>

<!-- layout/item_message_received.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="start"
    android:padding="8dp">

    <ImageView
        android:id="@+id/profileImage"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_marginEnd="8dp"
        android:scaleType="centerCrop"
        app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle"
        android:contentDescription="Profile image" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="start">

        <com.google.android.material.card.MaterialCardView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="60dp"
            app:cardBackgroundColor="@color/white"
            app:cardCornerRadius="16dp"
            app:cardElevation="2dp">

            <TextView
                android:id="@+id/messageText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="12dp"
                android:text="This is a received message"
                android:textColor="@color/black"
                android:textSize="16sp" />

        </com.google.android.material.card.MaterialCardView>

        <TextView
            android:id="@+id/timeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="4dp"
            android:text="10:30 AM"
            android:textColor="@color/gray"
            android:textSize="12sp" />

    </LinearLayout>

</LinearLayout>

Step 3: Chat Fragment Code

Now for the main chat functionality:

// ui/chat/ChatFragment.kt
@AndroidEntryPoint
class ChatFragment : Fragment() {

    private var _binding: FragmentChatBinding? = null
    private val binding get() = _binding!!

    private val viewModel: ChatViewModel by viewModels()

    private lateinit var messagesAdapter: MessagesAdapter
    private var matchId: String? = null
    private var otherUserId: String? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentChatBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        getArgs()
        setupToolbar()
        setupAdapter()
        setupObservers()
        setupClickListeners()
        loadMessages()

        Log.d("ChatFragment", "User entered chat with ${otherUserId ?: "unknown user"}")
    }

    private fun getArgs() {
        arguments?.let { args ->
            matchId = ChatFragmentArgs.fromBundle(args).matchId
            otherUserId = ChatFragmentArgs.fromBundle(args).otherUserId
            viewModel.setMatchInfo(matchId!!, otherUserId!!)
        }
    }

    private fun setupToolbar() {
        binding.toolbar.setNavigationOnClickListener {
            findNavController().navigateUp()
        }

        binding.menuButton.setOnClickListener {
            showChatMenu()
        }
    }

    private fun setupAdapter() {
        messagesAdapter = MessagesAdapter(getCurrentUserId())
        binding.messagesRecyclerView.apply {
            adapter = messagesAdapter
            layoutManager = LinearLayoutManager(requireContext()).apply {
                stackFromEnd = true
            }
            addItemDecoration(MessageItemDecoration())
        }

        // Auto-scroll to bottom when new messages arrive
        messagesAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
                binding.messagesRecyclerView.smoothScrollToPosition(messagesAdapter.itemCount)
            }
        })
    }

    private fun setupObservers() {
        viewModel.messages.observe(viewLifecycleOwner) { messages ->
            messagesAdapter.submitList(messages)
            if (messages.isNotEmpty()) {
                binding.messagesRecyclerView.scrollToPosition(messages.size - 1)
            }
        }

        viewModel.otherUser.observe(viewLifecycleOwner) { user ->
            user?.let {
                binding.userName.text = it.name
                binding.onlineStatus.text = if (it.isOnline) "Online" else "Last seen ${getRelativeTime(it.lastActive)}"
                binding.onlineStatus.setTextColor(
                    ContextCompat.getColor(
                        requireContext(),
                        if (it.isOnline) R.color.green else R.color.gray
                    )
                )

                // Load profile image
                Glide.with(requireContext())
                    .load(it.getMainPhotoUrl())
                    .placeholder(R.drawable.ic_profile_placeholder)
                    .error(R.drawable.ic_profile_error)
                    .circleCrop()
                    .into(binding.profileImage)
            }
        }

        viewModel.isTyping.observe(viewLifecycleOwner) { isTyping ->
            binding.typingIndicator.visibility = if (isTyping) View.VISIBLE else View.GONE
            if (isTyping) {
                startTypingAnimation()
            }
        }

        viewModel.error.observe(viewLifecycleOwner) { error ->
            error?.let {
                showError(it)
                Log.e("ChatFragment", "Chat error: $it")
            }
        }
    }

    private fun setupClickListeners() {
        binding.sendButton.setOnClickListener {
            sendMessage()
        }

        binding.messageInput.setOnKeyListener { _, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) {
                if (event.isShiftPressed) {
                    // Allow new line
                    return@setOnKeyListener false
                } else {
                    sendMessage()
                    return@setOnKeyListener true
                }
            }
            false
        }

        binding.messageInput.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

            override fun afterTextChanged(s: Editable?) {
                // Send typing indicator
                viewModel.setTyping(s?.isNotEmpty() == true)
            }
        })

        binding.attachmentButton.setOnClickListener {
            showAttachmentOptions()
        }
    }

    private fun sendMessage() {
        val message = binding.messageInput.text.toString().trim()
        if (message.isNotEmpty()) {
            viewModel.sendMessage(message)
            binding.messageInput.text?.clear()
        }
    }

    private fun loadMessages() {
        viewModel.loadMessages()
    }

    private fun showChatMenu() {
        val menuItems = arrayOf("View Profile", "Unmatch", "Report", "Block")

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Chat Options")
            .setItems(menuItems) { _, which ->
                when (which) {
                    0 -> viewProfile()
                    1 -> unmatch()
                    2 -> reportUser()
                    3 -> blockUser()
                }
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun showAttachmentOptions() {
        val options = arrayOf("Take Photo", "Choose from Gallery", "Send Location", "Send GIF")

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Send Attachment")
            .setItems(options) { _, which ->
                when (which) {
                    0 -> takePhoto()
                    1 -> chooseFromGallery()
                    2 -> sendLocation()
                    3 -> sendGif()
                }
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun viewProfile() {
        otherUserId?.let { userId ->
            findNavController().navigate(
                ChatFragmentDirections.actionChatFragmentToProfileDetailFragment(userId)
            )
        }
    }

    private fun unmatch() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Unmatch")
            .setMessage("Are you sure you want to unmatch? This action cannot be undone.")
            .setPositiveButton("Unmatch") { dialog, _ ->
                viewModel.unmatch()
                findNavController().navigateUp()
                Snackbar.make(binding.root, "Unmatched successfully", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun reportUser() {
        val reasons = arrayOf("Inappropriate messages", "Fake profile", "Spam", "Other")

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Report User")
            .setItems(reasons) { _, which ->
                viewModel.reportUser(reasons[which])
                Snackbar.make(binding.root, "User reported successfully", Snackbar.LENGTH_SHORT).show()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun blockUser() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Block User")
            .setMessage("Are you sure you want to block this user? You will no longer be able to message each other.")
            .setPositiveButton("Block") { dialog, _ ->
                viewModel.blockUser()
                findNavController().navigateUp()
                Snackbar.make(binding.root, "User blocked successfully", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun takePhoto() {
        // Implement camera intent
        Snackbar.make(binding.root, "Camera feature coming soon!", Snackbar.LENGTH_SHORT).show()
    }

    private fun chooseFromGallery() {
        // Implement gallery intent
        Snackbar.make(binding.root, "Gallery feature coming soon!", Snackbar.LENGTH_SHORT).show()
    }

    private fun sendLocation() {
        viewModel.sendLocation()
        Snackbar.make(binding.root, "Location sent!", Snackbar.LENGTH_SHORT).show()
    }

    private fun sendGif() {
        // Implement GIF picker
        Snackbar.make(binding.root, "GIF feature coming soon!", Snackbar.LENGTH_SHORT).show()
    }

    private fun startTypingAnimation() {
        val dots = binding.typingIndicator.getChildAt(1) as LinearLayout
        for (i in 0 until dots.childCount) {
            val dot = dots.getChildAt(i)
            dot.animate()
                .alpha(1f)
                .setDuration(400)
                .setStartDelay(i * 200L)
                .withEndAction {
                    dot.animate()
                        .alpha(0.4f)
                        .setDuration(400)
                        .start()
                }
                .start()
        }
    }

    private fun getRelativeTime(date: Date): String {
        // Same implementation as before
        val now = Date()
        val diff = now.time - date.time
        val minutes = diff / (60 * 1000)
        val hours = diff / (60 * 60 * 1000)

        return when {
            minutes < 1 -> "just now"
            minutes < 60 -> "${minutes.toInt()}m ago"
            hours < 24 -> "${hours.toInt()}h ago"
            else -> SimpleDateFormat("MMM dd", Locale.getDefault()).format(date)
        }
    }

    private fun getCurrentUserId(): String {
        return "current_user_id" // In reality, get from shared prefs or Firebase
    }

    private fun showError(message: String) {
        Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        Log.d("ChatFragment", "User left chat (probably ran out of things to say)")
    }
}

// Message item decoration for spacing
class MessageItemDecoration : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.top = 4
        outRect.bottom = 4
    }
}

What We've Built - The Complete Dating Experience!

🎉 HOLY CONVERSATIONS, BATMAN! We've just built a fully functional dating app that actually lets people talk to each other (or awkwardly stare at their screens)!

New Features Added:

  1. Matches Screen: Beautiful interface showing new matches and ongoing conversations
  2. Chat Interface: Full messaging system with real-time updates (simulated)
  3. Message Types: Support for sent/received messages with proper styling
  4. Typing Indicators: See when the other person is typing
  5. Online Status: Know when your match is available
  6. Chat Actions: Unmatch, report, block, and other safety features
  7. Attachment Support: Ready for photos, locations, and GIFs

Key Features Working:

Joke Break - Because Dating is Hard:

Why did the developer bring a phone to his date? In case he needed to call for help!

What's a programmer's favorite pickup line in the chat? "Are you a stack overflow? Because you've got all the answers I'm looking for."

Why was the chat conversation so short? They both waited for the other person to message first!

Your dating app now has all the core functionality users expect:

The app is now genuinely usable and could actually help people connect! In the next part, we'll add user profiles, settings, push notifications, and maybe even some AI-powered conversation starters. But for now, celebrate - you've built something that could potentially create real human connections (or at least provide some entertaining conversations)! 🚀💕

Next up: User profiles, settings, and making this thing production-ready!

Part 3: Android Dating App - Or, "Making It Actually Useful (And Maybe Profitable)"

Welcome back, you entrepreneurial cupid! We've built the core dating features, but now it's time to make this app something people would actually use (and maybe pay for). Let's add user profiles, settings, and all the polish that separates amateur apps from professional ones!

Chapter 3.1: User Profiles - "Where People Pretend to Be Interesting"

Step 1: Profile Fragment Layout - The Digital Resume of Love

Create layout/fragment_profile.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/light_gray"
    tools:context=".ui.profile.ProfileFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- Profile Header -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            app:cardCornerRadius="16dp"
            app:cardElevation="4dp">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="24dp">

                <!-- Profile Image -->
                <ImageView
                    android:id="@+id/profileImage"
                    android:layout_width="100dp"
                    android:layout_height="100dp"
                    android:layout_centerHorizontal="true"
                    android:scaleType="centerCrop"
                    app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Circle"
                    android:contentDescription="Profile image" />

                <!-- Edit Photo Button -->
                <com.google.android.material.floatingactionbutton.FloatingActionButton
                    android:id="@+id/editPhotoButton"
                    android:layout_width="36dp"
                    android:layout_height="36dp"
                    android:layout_alignEnd="@id/profileImage"
                    android:layout_alignBottom="@id/profileImage"
                    android:layout_marginEnd="-8dp"
                    android:layout_marginBottom="-8dp"
                    android:src="@drawable/ic_edit"
                    app:backgroundTint="@color/purple_500"
                    app:fabSize="mini"
                    app:tint="@color/white" />

                <!-- Name and Age -->
                <TextView
                    android:id="@+id/nameAgeText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/profileImage"
                    android:layout_centerHorizontal="true"
                    android:layout_marginTop="16dp"
                    android:text="Emma, 28"
                    android:textColor="@color/black"
                    android:textSize="24sp"
                    android:textStyle="bold" />

                <!-- Location -->
                <TextView
                    android:id="@+id/locationText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/nameAgeText"
                    android:layout_centerHorizontal="true"
                    android:layout_marginTop="4dp"
                    android:text="New York, NY"
                    android:textColor="@color/gray"
                    android:textSize="16sp" />

                <!-- Edit Profile Button -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/editProfileButton"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_below="@id/locationText"
                    android:layout_centerHorizontal="true"
                    android:layout_marginTop="16dp"
                    android:text="Edit Profile"
                    android:textColor="@color/purple_500"
                    app:strokeColor="@color/purple_500"
                    app:strokeWidth="1dp"
                    style="@style/Widget.Material3.Button.OutlinedButton" />

            </RelativeLayout>

        </com.google.android.material.card.MaterialCardView>

        <!-- Photos Section -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Photos"
                    android:textColor="@color/black"
                    android:textSize="18sp"
                    android:textStyle="bold" />

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/photosRecyclerView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="12dp"
                    tools:listitem="@layout/item_profile_photo" />

                <com.google.android.material.button.MaterialButton
                    android:id="@+id/addPhotosButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="12dp"
                    android:text="Add More Photos"
                    android:textColor="@color/purple_500"
                    style="@style/Widget.Material3.Button.OutlinedButton" />

            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

        <!-- Bio Section -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="About Me"
                    android:textColor="@color/black"
                    android:textSize="18sp"
                    android:textStyle="bold" />

                <TextView
                    android:id="@+id/bioText"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:text="Professional dog petter, amateur human. Looking for someone who doesn't mind my obsession with true crime podcasts."
                    android:textColor="@color/black"
                    android:textSize="16sp"
                    android:lineSpacingExtra="4dp" />

                <com.google.android.material.button.MaterialButton
                    android:id="@+id/editBioButton"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:layout_marginTop="8dp"
                    android:text="Edit Bio"
                    android:textColor="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

        <!-- Interests Section -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Interests"
                    android:textColor="@color/black"
                    android:textSize="18sp"
                    android:textStyle="bold" />

                <FlowLayout
                    android:id="@+id/interestsLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="12dp" />

                <com.google.android.material.button.MaterialButton
                    android:id="@+id/editInterestsButton"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:layout_marginTop="8dp"
                    android:text="Edit Interests"
                    android:textColor="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

        <!-- Settings Section -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="24dp"
            app:cardCornerRadius="12dp"
            app:cardElevation="2dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="8dp">

                <!-- Account Settings -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/accountSettingsButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    android:gravity="start"
                    android:text="Account Settings"
                    android:textColor="@color/black"
                    app:icon="@drawable/ic_account"
                    app:iconTint="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

                <!-- Discovery Settings -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/discoverySettingsButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    android:gravity="start"
                    android:text="Discovery Settings"
                    android:textColor="@color/black"
                    app:icon="@drawable/ic_discovery"
                    app:iconTint="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

                <!-- Notifications -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/notificationsButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    android:gravity="start"
                    android:text="Notifications"
                    android:textColor="@color/black"
                    app:icon="@drawable/ic_notifications"
                    app:iconTint="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

                <!-- Privacy & Safety -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/privacyButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    android:gravity="start"
                    android:text="Privacy & Safety"
                    android:textColor="@color/black"
                    app:icon="@drawable/ic_privacy"
                    app:iconTint="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

                <!-- Help & Support -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/helpButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    android:gravity="start"
                    android:text="Help & Support"
                    android:textColor="@color/black"
                    app:icon="@drawable/ic_help"
                    app:iconTint="@color/purple_500"
                    style="@style/Widget.Material3.Button.TextButton" />

                <!-- Logout -->
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/logoutButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    android:gravity="start"
                    android:text="Logout"
                    android:textColor="@color/red"
                    app:icon="@drawable/ic_logout"
                    app:iconTint="@color/red"
                    style="@style/Widget.Material3.Button.TextButton" />

            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

    </LinearLayout>

</androidx.core.widget.NestedScrollView>

Step 2: Profile Photo Item Layout

Create layout/item_profile_photo.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="100dp"
    android:layout_height="150dp"
    android:layout_margin="4dp">

    <ImageView
        android:id="@+id/photoImage"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:contentDescription="Profile photo"
        app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.MediumComponent" />

    <!-- Main Photo Badge -->
    <TextView
        android:id="@+id/mainPhotoBadge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:layout_margin="4dp"
        android:background="@drawable/badge_main_photo"
        android:paddingHorizontal="8dp"
        android:paddingVertical="2dp"
        android:text="MAIN"
        android:textColor="@color/white"
        android:textSize="10sp"
        android:textStyle="bold"
        android:visibility="gone" />

    <!-- Delete Button -->
    <ImageButton
        android:id="@+id/deleteButton"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_margin="4dp"
        android:background="@drawable/circle_background_red"
        android:src="@drawable/ic_close"
        android:contentDescription="Delete photo"
        app:tint="@color/white" />

</RelativeLayout>

Step 3: FlowLayout for Interests

We need a custom FlowLayout for the interests. Create this utility class:

// util/FlowLayout.kt
class FlowLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    private var lineHeight = 0

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        assert(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED)
        val width = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight
        var height = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom
        val count = childCount
        var lineHeight = 0
        var xPos = paddingLeft
        var yPos = paddingTop
        val childHeightMeasureSpec = if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
            MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)
        } else {
            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
        }
        for (i in 0 until count) {
            val child = getChildAt(i)
            if (child.visibility != View.GONE) {
                child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), childHeightMeasureSpec)
                val childw = child.measuredWidth
                lineHeight = max(lineHeight, child.measuredHeight + 8)
                if (xPos + childw > width) {
                    xPos = paddingLeft
                    yPos += lineHeight
                }
                xPos += childw + 8
            }
        }
        this.lineHeight = lineHeight
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
            height = yPos + lineHeight
        } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
            if (yPos + lineHeight < height) {
                height = yPos + lineHeight
            }
        }
        setMeasuredDimension(width, height + paddingBottom)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val count = childCount
        val width = r - l
        var xPos = paddingLeft
        var yPos = paddingTop
        for (i in 0 until count) {
            val child = getChildAt(i)
            if (child.visibility != View.GONE) {
                val childw = child.measuredWidth
                val childh = child.measuredHeight
                if (xPos + childw > width) {
                    xPos = paddingLeft
                    yPos += lineHeight
                }
                child.layout(xPos, yPos, xPos + childw, yPos + childh)
                xPos += childw + 8
            }
        }
    }
}

Step 4: Profile Fragment Code

Now let's create the ProfileFragment:

// ui/profile/ProfileFragment.kt
@AndroidEntryPoint
class ProfileFragment : Fragment() {

    private var _binding: FragmentProfileBinding? = null
    private val binding get() = _binding!!

    private val viewModel: ProfileViewModel by viewModels()
    private lateinit var photosAdapter: ProfilePhotosAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentProfileBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupAdapters()
        setupClickListeners()
        setupObservers()
        loadProfile()

        Log.d("ProfileFragment", "User is admiring their own profile (probably)")
    }

    private fun setupAdapters() {
        photosAdapter = ProfilePhotosAdapter { photoUrl, position ->
            if (position == 0) {
                // Main photo - view full screen
                viewPhotoFullScreen(photoUrl)
            } else {
                // Other photo - options menu
                showPhotoOptions(photoUrl, position)
            }
        }

        binding.photosRecyclerView.apply {
            adapter = photosAdapter
            layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
            addItemDecoration(SpacingItemDecoration(8))
        }
    }

    private fun setupClickListeners() {
        binding.editProfileButton.setOnClickListener {
            editProfile()
        }

        binding.editPhotoButton.setOnClickListener {
            changeProfilePhoto()
        }

        binding.addPhotosButton.setOnClickListener {
            addMorePhotos()
        }

        binding.editBioButton.setOnClickListener {
            editBio()
        }

        binding.editInterestsButton.setOnClickListener {
            editInterests()
        }

        // Settings buttons
        binding.accountSettingsButton.setOnClickListener {
            openAccountSettings()
        }

        binding.discoverySettingsButton.setOnClickListener {
            openDiscoverySettings()
        }

        binding.notificationsButton.setOnClickListener {
            openNotificationSettings()
        }

        binding.privacyButton.setOnClickListener {
            openPrivacySettings()
        }

        binding.helpButton.setOnClickListener {
            openHelpSupport()
        }

        binding.logoutButton.setOnClickListener {
            logout()
        }
    }

    private fun setupObservers() {
        viewModel.userProfile.observe(viewLifecycleOwner) { user ->
            user?.let {
                updateUI(it)
            }
        }

        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
            if (isLoading) {
                showLoading()
            } else {
                hideLoading()
            }
        }

        viewModel.error.observe(viewLifecycleOwner) { error ->
            error?.let {
                showError(it)
                Log.e("ProfileFragment", "Error loading profile: $it")
            }
        }
    }

    private fun loadProfile() {
        viewModel.loadProfile()
    }

    private fun updateUI(user: User) {
        binding.nameAgeText.text = "${user.name}, ${user.age}"
        binding.locationText.text = user.location
        binding.bioText.text = user.bio.ifEmpty { "Tell people something about yourself..." }

        // Load profile image
        if (user.photoUrls.isNotEmpty()) {
            Glide.with(requireContext())
                .load(user.photoUrls[0])
                .placeholder(R.drawable.ic_profile_placeholder)
                .error(R.drawable.ic_profile_error)
                .circleCrop()
                .into(binding.profileImage)
        }

        // Load photos
        photosAdapter.submitList(user.photoUrls)

        // Load interests
        loadInterests(user.interests)
    }

    private fun loadInterests(interests: List<String>) {
        binding.interestsLayout.removeAllViews()

        if (interests.isEmpty()) {
            val placeholder = TextView(requireContext()).apply {
                text = "Add some interests to show people what you're passionate about!"
                setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
                textSize = 14f
            }
            binding.interestsLayout.addView(placeholder)
            return
        }

        interests.forEach { interest ->
            val chip = Chip(requireContext()).apply {
                text = interest
                isClickable = false
                isCheckable = false
                setChipBackgroundColorResource(R.color.chip_background)
                setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
                chipStrokeWidth = 0f
            }
            binding.interestsLayout.addView(chip)
        }
    }

    private fun editProfile() {
        findNavController().navigate(
            ProfileFragmentDirections.actionProfileFragmentToEditProfileFragment()
        )
    }

    private fun changeProfilePhoto() {
        val options = arrayOf("Take Photo", "Choose from Gallery", "Remove Current Photo")

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Change Profile Photo")
            .setItems(options) { _, which ->
                when (which) {
                    0 -> takePhoto()
                    1 -> chooseFromGallery()
                    2 -> removeCurrentPhoto()
                }
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun addMorePhotos() {
        val remainingSlots = 6 - (photosAdapter.itemCount)
        if (remainingSlots <= 0) {
            Snackbar.make(binding.root, "You've reached the maximum number of photos", Snackbar.LENGTH_SHORT).show()
            return
        }

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Add Photos")
            .setMessage("You can add $remainingSlots more photos. Where would you like to choose from?")
            .setPositiveButton("Gallery") { dialog, _ ->
                // Implement gallery picker
                Snackbar.make(binding.root, "Gallery picker coming soon!", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun editBio() {
        val input = TextInputEditText(requireContext()).apply {
            setText(binding.bioText.text)
            hint = "Tell people about yourself..."
            setLines(3)
            maxLines = 5
        }

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Edit Bio")
            .setView(input)
            .setPositiveButton("Save") { dialog, _ ->
                val newBio = input.text.toString().trim()
                if (newBio != binding.bioText.text) {
                    viewModel.updateBio(newBio)
                    binding.bioText.text = newBio
                    Snackbar.make(binding.root, "Bio updated successfully", Snackbar.LENGTH_SHORT).show()
                }
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun editInterests() {
        val interests = listOf(
            "Hiking", "Cooking", "Movies", "Travel", "Music", "Gaming",
            "Reading", "Sports", "Art", "Photography", "Dancing", "Yoga",
            "Technology", "Foodie", "Fitness", "Nature", "Coffee", "Books",
            "Concerts", "Pets", "Wine", "Beer", "Fashion", "Cars"
        )

        val currentInterests = viewModel.userProfile.value?.interests ?: emptyList()
        val checkedItems = BooleanArray(interests.size) { i ->
            interests[i] in currentInterests
        }

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Select Interests")
            .setMultiChoiceItems(interests.toTypedArray(), checkedItems) { _, which, isChecked ->
                checkedItems[which] = isChecked
            }
            .setPositiveButton("Save") { dialog, _ ->
                val selectedInterests = interests.filterIndexed { index, _ -> checkedItems[index] }
                viewModel.updateInterests(selectedInterests)
                loadInterests(selectedInterests)
                Snackbar.make(binding.root, "Interests updated successfully", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .setNeutralButton("Clear All") { dialog, _ ->
                viewModel.updateInterests(emptyList())
                loadInterests(emptyList())
                Snackbar.make(binding.root, "Interests cleared", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .show()
    }

    private fun openAccountSettings() {
        findNavController().navigate(
            ProfileFragmentDirections.actionProfileFragmentToAccountSettingsFragment()
        )
    }

    private fun openDiscoverySettings() {
        findNavController().navigate(
            ProfileFragmentDirections.actionProfileFragmentToDiscoverySettingsFragment()
        )
    }

    private fun openNotificationSettings() {
        findNavController().navigate(
            ProfileFragmentDirections.actionProfileFragmentToNotificationSettingsFragment()
        )
    }

    private fun openPrivacySettings() {
        findNavController().navigate(
            ProfileFragmentDirections.actionProfileFragmentToPrivacySettingsFragment()
        )
    }

    private fun openHelpSupport() {
        findNavController().navigate(
            ProfileFragmentDirections.actionProfileFragmentToHelpSupportFragment()
        )
    }

    private fun logout() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Logout")
            .setMessage("Are you sure you want to logout?")
            .setPositiveButton("Logout") { dialog, _ ->
                viewModel.logout()
                // Navigate to login screen
                findNavController().navigate(R.id.loginFragment)
                Snackbar.make(binding.root, "Logged out successfully", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun takePhoto() {
        // Implement camera intent
        Snackbar.make(binding.root, "Camera feature coming soon!", Snackbar.LENGTH_SHORT).show()
    }

    private fun chooseFromGallery() {
        // Implement gallery intent
        Snackbar.make(binding.root, "Gallery feature coming soon!", Snackbar.LENGTH_SHORT).show()
    }

    private fun removeCurrentPhoto() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Remove Photo")
            .setMessage("Are you sure you want to remove your profile photo?")
            .setPositiveButton("Remove") { dialog, _ ->
                viewModel.removeProfilePhoto()
                Snackbar.make(binding.root, "Profile photo removed", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun viewPhotoFullScreen(photoUrl: String) {
        // Implement full screen photo viewer
        Snackbar.make(binding.root, "Full screen viewer coming soon!", Snackbar.LENGTH_SHORT).show()
    }

    private fun showPhotoOptions(photoUrl: String, position: Int) {
        val options = arrayOf("Set as Main", "Delete Photo")

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Photo Options")
            .setItems(options) { _, which ->
                when (which) {
                    0 -> setAsMainPhoto(position)
                    1 -> deletePhoto(position)
                }
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun setAsMainPhoto(position: Int) {
        viewModel.setMainPhoto(position)
        Snackbar.make(binding.root, "Photo set as main", Snackbar.LENGTH_SHORT).show()
    }

    private fun deletePhoto(position: Int) {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Delete Photo")
            .setMessage("Are you sure you want to delete this photo?")
            .setPositiveButton("Delete") { dialog, _ ->
                viewModel.deletePhoto(position)
                Snackbar.make(binding.root, "Photo deleted", Snackbar.LENGTH_SHORT).show()
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun showLoading() {
        // Show loading indicator
        binding.root.findViewById<ProgressBar>(R.id.loadingIndicator)?.visibility = View.VISIBLE
    }

    private fun hideLoading() {
        binding.root.findViewById<ProgressBar>(R.id.loadingIndicator)?.visibility = View.GONE
    }

    private fun showError(message: String) {
        Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG)
            .setAction("Retry") { loadProfile() }
            .show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

// Profile Photos Adapter
class ProfilePhotosAdapter(
    private val onPhotoClick: (String, Int) -> Unit
) : ListAdapter<String, ProfilePhotosAdapter.PhotoViewHolder>(PhotoDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
        val binding = ItemProfilePhotoBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return PhotoViewHolder(binding)
    }

    override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
        val photoUrl = getItem(position)
        holder.bind(photoUrl, position)
    }

    inner class PhotoViewHolder(
        private val binding: ItemProfilePhotoBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val photoUrl = getItem(position)
                    onPhotoClick(photoUrl, position)
                }
            }
        }

        fun bind(photoUrl: String, position: Int) {
            // Load photo
            Glide.with(binding.root.context)
                .load(photoUrl)
                .placeholder(R.drawable.ic_profile_placeholder)
                .error(R.drawable.ic_profile_error)
                .centerCrop()
                .into(binding.photoImage)

            // Show main photo badge for first photo
            binding.mainPhotoBadge.visibility = if (position == 0) View.VISIBLE else View.GONE

            // Hide delete button for main photo (can't delete your only photo)
            binding.deleteButton.visibility = if (position == 0 && itemCount == 1) View.GONE else View.VISIBLE

            binding.deleteButton.setOnClickListener {
                val pos = bindingAdapterPosition
                if (pos != RecyclerView.NO_POSITION) {
                    onPhotoClick(photoUrl, pos)
                }
            }
        }
    }

    class PhotoDiffCallback : DiffUtil.ItemCallback<String>() {
        override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
            return oldItem == newItem
        }
    }
}

Step 5: Profile ViewModel

Create the ProfileViewModel:

// ui/profile/ProfileViewModel.kt
@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val userRepository: UserRepository,
    private val authRepository: AuthRepository
) : ViewModel() {

    private val _userProfile = MutableLiveData<User>()
    val userProfile: LiveData<User> = _userProfile

    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading

    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error

    init {
        Log.d("ProfileViewModel", "ProfileViewModel initialized")
    }

    fun loadProfile() {
        _isLoading.value = true
        _error.value = null

        viewModelScope.launch {
            try {
                val profile = userRepository.getCurrentUserProfile()
                _userProfile.value = profile
                Log.d("ProfileViewModel", "Profile loaded: ${profile.name}")
            } catch (e: Exception) {
                _error.value = "Failed to load profile: ${e.message}"
                Log.e("ProfileViewModel", "Error loading profile", e)
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun updateBio(newBio: String) {
        viewModelScope.launch {
            try {
                userRepository.updateBio(newBio)
                // Update local profile
                _userProfile.value = _userProfile.value?.copy(bio = newBio)
                Log.d("ProfileViewModel", "Bio updated: $newBio")
            } catch (e: Exception) {
                _error.value = "Failed to update bio: ${e.message}"
                Log.e("ProfileViewModel", "Error updating bio", e)
            }
        }
    }

    fun updateInterests(newInterests: List<String>) {
        viewModelScope.launch {
            try {
                userRepository.updateInterests(newInterests)
                // Update local profile
                _userProfile.value = _userProfile.value?.copy(interests = newInterests)
                Log.d("ProfileViewModel", "Interests updated: ${newInterests.size} interests")
            } catch (e: Exception) {
                _error.value = "Failed to update interests: ${e.message}"
                Log.e("ProfileViewModel", "Error updating interests", e)
            }
        }
    }

    fun removeProfilePhoto() {
        viewModelScope.launch {
            try {
                userRepository.removeProfilePhoto()
                // Update local profile - remove first photo
                val currentPhotos = _userProfile.value?.photoUrls ?: emptyList()
                val newPhotos = if (currentPhotos.size > 1) currentPhotos.drop(1) else emptyList()
                _userProfile.value = _userProfile.value?.copy(photoUrls = newPhotos)
                Log.d("ProfileViewModel", "Profile photo removed")
            } catch (e: Exception) {
                _error.value = "Failed to remove profile photo: ${e.message}"
                Log.e("ProfileViewModel", "Error removing profile photo", e)
            }
        }
    }

    fun setMainPhoto(position: Int) {
        viewModelScope.launch {
            try {
                val currentPhotos = _userProfile.value?.photoUrls ?: emptyList()
                if (position in currentPhotos.indices) {
                    val newMainPhoto = currentPhotos[position]
                    val otherPhotos = currentPhotos.filterIndexed { index, _ -> index != position }
                    val newPhotos = listOf(newMainPhoto) + otherPhotos

                    userRepository.updatePhotos(newPhotos)
                    _userProfile.value = _userProfile.value?.copy(photoUrls = newPhotos)
                    Log.d("ProfileViewModel", "Main photo updated to position $position")
                }
            } catch (e: Exception) {
                _error.value = "Failed to update main photo: ${e.message}"
                Log.e("ProfileViewModel", "Error updating main photo", e)
            }
        }
    }

    fun deletePhoto(position: Int) {
        viewModelScope.launch {
            try {
                val currentPhotos = _userProfile.value?.photoUrls ?: emptyList()
                if (position in currentPhotos.indices) {
                    val newPhotos = currentPhotos.filterIndexed { index, _ -> index != position }
                    userRepository.updatePhotos(newPhotos)
                    _userProfile.value = _userProfile.value?.copy(photoUrls = newPhotos)
                    Log.d("ProfileViewModel", "Photo at position $position deleted")
                }
            } catch (e: Exception) {
                _error.value = "Failed to delete photo: ${e.message}"
                Log.e("ProfileViewModel", "Error deleting photo", e)
            }
        }
    }

    fun logout() {
        viewModelScope.launch {
            try {
                authRepository.logout()
                Log.d("ProfileViewModel", "User logged out successfully")
            } catch (e: Exception) {
                _error.value = "Failed to logout: ${e.message}"
                Log.e("ProfileViewModel", "Error during logout", e)
            }
        }
    }
}

Chapter 3.2: Settings Fragments - "Where We Ask for All the Permissions"

Step 1: Discovery Settings Fragment

Create layout/fragment_discovery_settings.xml and the corresponding fragment:

// ui/settings/DiscoverySettingsFragment.kt
@AndroidEntryPoint
class DiscoverySettingsFragment : Fragment() {

    private var _binding: FragmentDiscoverySettingsBinding? = null
    private val binding get() = _binding!!

    private val viewModel: DiscoverySettingsViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentDiscoverySettingsBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupToolbar()
        setupClickListeners()
        setupObservers()
        loadSettings()

        Log.d("DiscoverySettings", "User is being picky about who they see")
    }

    private fun setupToolbar() {
        binding.toolbar.setNavigationOnClickListener {
            findNavController().navigateUp()
        }
    }

    private fun setupClickListeners() {
        // Gender preferences
        binding.showMenSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateGenderPreference("men", isChecked)
        }

        binding.showWomenSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateGenderPreference("women", isChecked)
        }

        binding.showEveryoneSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateGenderPreference("everyone", isChecked)
        }

        // Age range
        binding.ageRangeSlider.addOnChangeListener { slider, value, fromUser ->
            if (fromUser) {
                val values = slider.values
                viewModel.updateAgeRange(values[0].toInt(), values[1].toInt())
                binding.ageRangeText.text = "${values[0].toInt()} - ${values[1].toInt()} years"
            }
        }

        // Distance
        binding.distanceSlider.addOnChangeListener { slider, value, fromUser ->
            if (fromUser) {
                viewModel.updateDistance(value.toInt())
                binding.distanceText.text = "Within ${value.toInt()} km"
            }
        }

        // Global switch
        binding.globalSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateGlobalSetting(isChecked)
            binding.distanceSection.visibility = if (isChecked) View.GONE else View.VISIBLE
        }
    }

    private fun setupObservers() {
        viewModel.settings.observe(viewLifecycleOwner) { settings ->
            updateUI(settings)
        }

        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
            if (isLoading) {
                showLoading()
            } else {
                hideLoading()
            }
        }
    }

    private fun loadSettings() {
        viewModel.loadSettings()
    }

    private fun updateUI(settings: DiscoverySettings) {
        // Gender preferences
        binding.showMenSwitch.isChecked = settings.showMen
        binding.showWomenSwitch.isChecked = settings.showWomen
        binding.showEveryoneSwitch.isChecked = settings.showEveryone

        // Age range
        binding.ageRangeSlider.setValues(settings.minAge.toFloat(), settings.maxAge.toFloat())
        binding.ageRangeText.text = "${settings.minAge} - ${settings.maxAge} years"

        // Distance
        binding.distanceSlider.value = settings.maxDistance.toFloat()
        binding.distanceText.text = "Within ${settings.maxDistance} km"

        // Global setting
        binding.globalSwitch.isChecked = settings.isGlobal
        binding.distanceSection.visibility = if (settings.isGlobal) View.GONE else View.VISIBLE
    }

    private fun showLoading() {
        binding.progressBar.visibility = View.VISIBLE
    }

    private fun hideLoading() {
        binding.progressBar.visibility = View.GONE
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

// Discovery Settings Data Class
data class DiscoverySettings(
    val showMen: Boolean = true,
    val showWomen: Boolean = true,
    val showEveryone: Boolean = false,
    val minAge: Int = 18,
    val maxAge: Int = 35,
    val maxDistance: Int = 50,
    val isGlobal: Boolean = false
)

Chapter 3.3: Push Notifications - "Because People Need Constant Validation"

Step 1: Notification Service

Create a notification service to handle push notifications:

// service/NotificationService.kt
class NotificationService : FirebaseMessagingService() {

    private val notificationManager by lazy { 
        getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 
    }

    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.d("NotificationService", "New FCM token: $token")

        // Send token to your server
        sendTokenToServer(token)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)

        Log.d("NotificationService", "Received message: ${remoteMessage.data}")

        // Handle different types of notifications
        when (remoteMessage.data["type"]) {
            "new_match" -> handleNewMatchNotification(remoteMessage)
            "new_message" -> handleNewMessageNotification(remoteMessage)
            "super_like" -> handleSuperLikeNotification(remoteMessage)
            "profile_view" -> handleProfileViewNotification(remoteMessage)
            else -> handleGenericNotification(remoteMessage)
        }
    }

    private fun handleNewMatchNotification(remoteMessage: RemoteMessage) {
        val title = remoteMessage.data["title"] ?: "It's a match! 🎉"
        val body = remoteMessage.data["body"] ?: "You and someone liked each other!"
        val matchId = remoteMessage.data["match_id"]
        val userId = remoteMessage.data["user_id"]

        createNotification(
            channelId = "matches",
            channelName = "Matches",
            title = title,
            body = body,
            largeIcon = remoteMessage.data["user_image"],
            intent = createMatchIntent(matchId, userId)
        )

        Log.d("NotificationService", "New match notification: $title")
    }

    private fun handleNewMessageNotification(remoteMessage: RemoteMessage) {
        val title = remoteMessage.data["sender_name"] ?: "New message"
        val body = remoteMessage.data["message"] ?: "You have a new message"
        val matchId = remoteMessage.data["match_id"]
        val messageId = remoteMessage.data["message_id"]

        createNotification(
            channelId = "messages",
            channelName = "Messages", 
            title = title,
            body = body,
            largeIcon = remoteMessage.data["sender_image"],
            intent = createChatIntent(matchId, messageId)
        )

        Log.d("NotificationService", "New message notification from: $title")
    }

    private fun handleSuperLikeNotification(remoteMessage: RemoteMessage) {
        val title = "Super Like! ⭐"
        val body = remoteMessage.data["body"] ?: "Someone really likes you!"
        val userId = remoteMessage.data["user_id"]

        createNotification(
            channelId = "likes",
            channelName = "Likes",
            title = title,
            body = body,
            largeIcon = remoteMessage.data["user_image"],
            intent = createProfileIntent(userId)
        )

        Log.d("NotificationService", "Super like notification")
    }

    private fun createNotification(
        channelId: String,
        channelName: String,
        title: String,
        body: String,
        largeIcon: String? = null,
        intent: PendingIntent? = null
    ) {
        // Create notification channel (required for Android 8.0+)
        createNotificationChannel(channelId, channelName)

        val notificationId = Random.nextInt(1000, 9999)

        val notificationBuilder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .setContentIntent(intent)
            .setStyle(NotificationCompat.BigTextStyle().bigText(body))

        // Set large icon if available
        largeIcon?.let { iconUrl ->
            try {
                val futureTarget = Glide.with(this)
                    .asBitmap()
                    .load(iconUrl)
                    .submit(100, 100)

                val bitmap = futureTarget.get()
                notificationBuilder.setLargeIcon(bitmap)
                Glide.with(this).clear(futureTarget)
            } catch (e: Exception) {
                Log.e("NotificationService", "Error loading notification image", e)
            }
        }

        // Add actions for messages
        if (channelId == "messages") {
            notificationBuilder.addAction(
                R.drawable.ic_reply,
                "Reply",
                createReplyIntent(notificationId)
            )
        }

        notificationManager.notify(notificationId, notificationBuilder.build())
    }

    private fun createNotificationChannel(channelId: String, channelName: String) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                channelName,
                NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "Notifications for $channelName"
                enableLights(true)
                lightColor = Color.BLUE
                enableVibration(true)
            }

            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun createMatchIntent(matchId: String?, userId: String?): PendingIntent {
        val intent = Intent(this, MainActivity::class.java).apply {
            putExtra("fragment", "matches")
            putExtra("match_id", matchId)
            putExtra("user_id", userId)
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }

        return PendingIntent.getActivity(
            this,
            Random.nextInt(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    private fun createChatIntent(matchId: String?, messageId: String?): PendingIntent {
        val intent = Intent(this, MainActivity::class.java).apply {
            putExtra("fragment", "chat")
            putExtra("match_id", matchId)
            putExtra("message_id", messageId)
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }

        return PendingIntent.getActivity(
            this,
            Random.nextInt(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    private fun sendTokenToServer(token: String) {
        // Send to your backend server
        Log.d("NotificationService", "Sending token to server: $token")
        // Implementation depends on your backend
    }
}

Step 2: Notification Settings Fragment

Create notification settings to let users control their preferences:

// ui/settings/NotificationSettingsFragment.kt
@AndroidEntryPoint
class NotificationSettingsFragment : Fragment() {

    private var _binding: FragmentNotificationSettingsBinding? = null
    private val binding get() = _binding!!

    private val viewModel: NotificationSettingsViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentNotificationSettingsBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupToolbar()
        setupClickListeners()
        setupObservers()
        loadSettings()

        Log.d("NotificationSettings", "User is deciding how annoyed they want to be")
    }

    private fun setupToolbar() {
        binding.toolbar.setNavigationOnClickListener {
            findNavController().navigateUp()
        }
    }

    private fun setupClickListeners() {
        // Toggle switches
        binding.newMatchesSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateSetting("new_matches", isChecked)
        }

        binding.messagesSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateSetting("messages", isChecked)
        }

        binding.superLikesSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateSetting("super_likes", isChecked)
        }

        binding.profileViewsSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateSetting("profile_views", isChecked)
        }

        binding.promotionsSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateSetting("promotions", isChecked)
        }

        binding.newsletterSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateSetting("newsletter", isChecked)
        }

        // Do Not Disturb
        binding.dndSwitch.setOnCheckedChangeListener { _, isChecked ->
            viewModel.updateDNDSetting(isChecked)
            binding.dndTimeLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
        }

        // DND Time
        binding.dndStartTime.setOnClickListener {
            showTimePicker(true)
        }

        binding.dndEndTime.setOnClickListener {
            showTimePicker(false)
        }

        // Test notification button
        binding.testNotificationButton.setOnClickListener {
            viewModel.sendTestNotification()
            Snackbar.make(binding.root, "Test notification sent!", Snackbar.LENGTH_SHORT).show()
        }
    }

    private fun setupObservers() {
        viewModel.settings.observe(viewLifecycleOwner) { settings ->
            updateUI(settings)
        }
    }

    private fun loadSettings() {
        viewModel.loadSettings()
    }

    private fun updateUI(settings: NotificationSettings) {
        binding.newMatchesSwitch.isChecked = settings.newMatches
        binding.messagesSwitch.isChecked = settings.messages
        binding.superLikesSwitch.isChecked = settings.superLikes
        binding.profileViewsSwitch.isChecked = settings.profileViews
        binding.promotionsSwitch.isChecked = settings.promotions
        binding.newsletterSwitch.isChecked = settings.newsletter
        binding.dndSwitch.isChecked = settings.doNotDisturb
        binding.dndTimeLayout.visibility = if (settings.doNotDisturb) View.VISIBLE else View.GONE
        binding.dndStartTime.text = settings.dndStartTime
        binding.dndEndTime.text = settings.dndEndTime
    }

    private fun showTimePicker(isStartTime: Boolean) {
        val currentTime = if (isStartTime) {
            parseTime(binding.dndStartTime.text.toString())
        } else {
            parseTime(binding.dndEndTime.text.toString())
        }

        TimePickerDialog(
            requireContext(),
            { _, hour, minute ->
                val timeString = String.format("%02d:%02d", hour, minute)
                if (isStartTime) {
                    binding.dndStartTime.text = timeString
                    viewModel.updateDNDTime(timeString, binding.dndEndTime.text.toString())
                } else {
                    binding.dndEndTime.text = timeString
                    viewModel.updateDNDTime(binding.dndStartTime.text.toString(), timeString)
                }
            },
            currentTime.first,
            currentTime.second,
            true
        ).show()
    }

    private fun parseTime(timeString: String): Pair<Int, Int> {
        return try {
            val parts = timeString.split(":")
            Pair(parts[0].toInt(), parts[1].toInt())
        } catch (e: Exception) {
            Pair(22, 0) // Default to 10:00 PM
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

// Notification Settings Data Class
data class NotificationSettings(
    val newMatches: Boolean = true,
    val messages: Boolean = true,
    val superLikes: Boolean = true,
    val profileViews: Boolean = false,
    val promotions: Boolean = false,
    val newsletter: Boolean = false,
    val doNotDisturb: Boolean = false,
    val dndStartTime: String = "22:00",
    val dndEndTime: String = "08:00"
)

What We've Built - A Production-Ready Dating App!

🎉 HOLY POLISH, BATMAN! We've just transformed our basic dating app into a professional, feature-rich platform that could actually compete in the market!

New Features Added:

  1. Complete User Profiles: Beautiful profile screens with photos, bio, and interests
  2. Profile Management: Edit photos, bio, interests with intuitive interfaces
  3. Discovery Settings: Advanced filters for age, distance, and gender preferences
  4. Push Notifications: Full notification system for matches, messages, and super likes
  5. Notification Settings: User control over what notifications they receive
  6. Settings Navigation: Complete settings hierarchy for account management

Key Production Features:

Joke Break - Because Settings Should Be Fun Too:

Why did the developer include so many settings? So users can customize their disappointment!

What's a programmer's favorite notification setting? "Do not disturb - unless it's a match"

Why was the profile picture round? So the disappointment doesn't have any sharp edges!

Your dating app now has all the features users expect from a modern dating platform:

The app is now genuinely production-ready and could be published on the Google Play Store! In the final part, we'll add some advanced features like AI-powered icebreakers, premium subscriptions, and analytics. But for now, you should be incredibly proud - you've built a complete, professional dating app from scratch! 🚀💕

Next up: Advanced features, monetization, and making this thing actually profitable!

Part 4: Android Dating App - Or, "Making Money from Love (And Other Advanced Features)"

Welcome back, you entrepreneurial cupid! We've built a solid dating app, but now it's time to make it truly exceptional (and profitable). Let's add AI-powered features, premium subscriptions, analytics, and all the bells and whistles that separate amateur apps from market leaders!

Chapter 4.1: AI-Powered Icebreakers - "Because 'Hey' is So 2015"

Step 1: AI Service Integration

First, let's create an AI service that generates personalized icebreakers:

// service/AIService.kt
interface AIService {
    suspend fun generateIcebreaker(userProfile: User, targetProfile: User): Result<String>
    suspend fun analyzeConversation(messages: List<Message>): ConversationAnalysis
    suspend fun suggestResponse(conversation: List<Message>): Result<String>
    suspend fun detectCompatibility(user1: User, user2: User): CompatibilityScore
}

// Implementation using a mock AI service (in reality, you'd use OpenAI, etc.)
class MockAIService @Inject constructor() : AIService {

    private val icebreakerTemplates = listOf(
        "I noticed we both like {interest}. What's your favorite thing about it?",
        "Your profile really stood out to me! How's your {dayTime} going?",
        "Fellow {interest} enthusiast! Have you tried any new {interest} activities lately?",
        "I'm curious about what brought you to {appName}. What's been your experience so far?",
        "Your smile is contagious! What's something that always makes you smile?",
        "I see you're into {interest1} and {interest2}. That's an interesting combination!",
        "As a {userJob}, what's the most exciting project you've worked on recently?",
        "Your travel photos look amazing! What's been your favorite destination so far?"
    )

    private val dayTimes = listOf("day", "week", "weekend")
    private val genericIcebreakers = listOf(
        "Hey! I'd love to get to know you better. What are you passionate about?",
        "Hi there! Your profile caught my eye. What's something you're excited about right now?",
        "Hello! I'm curious to learn more about you. What's your story?",
        "Hey! I noticed we have some things in common. Want to chat?",
        "Hi! I'd love to hear more about your adventures and experiences."
    )

    override suspend fun generateIcebreaker(userProfile: User, targetProfile: User): Result<String> {
        return try {
            // Simulate API delay
            delay(500 + Random.nextLong(1000))

            // Use template-based generation for demo
            val icebreaker = generatePersonalizedIcebreaker(userProfile, targetProfile)
            Result.success(icebreaker)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun analyzeConversation(messages: List<Message>): ConversationAnalysis {
        return ConversationAnalysis(
            engagementLevel = calculateEngagementLevel(messages),
            suggestedTopics = generateSuggestedTopics(messages),
            conversationStyle = detectConversationStyle(messages),
            redFlags = detectRedFlags(messages)
        )
    }

    override suspend fun suggestResponse(conversation: List<Message>): Result<String> {
        return try {
            delay(300 + Random.nextLong(800))

            val lastMessage = conversation.lastOrNull()?.content ?: ""
            val response = generateSmartResponse(lastMessage, conversation)
            Result.success(response)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun detectCompatibility(user1: User, user2: User): CompatibilityScore {
        return CompatibilityScore(
            overallScore = calculateCompatibility(user1, user2),
            interestsMatch = calculateInterestsCompatibility(user1.interests, user2.interests),
            conversationPotential = calculateConversationPotential(user1, user2),
            lifestyleAlignment = calculateLifestyleAlignment(user1, user2),
            strengths = generateCompatibilityStrengths(user1, user2),
            considerations = generateCompatibilityConsiderations(user1, user2)
        )
    }

    private fun generatePersonalizedIcebreaker(user: User, target: User): String {
        // Try to find common interests
        val commonInterests = user.interests.intersect(target.interests.toSet())

        return when {
            commonInterests.isNotEmpty() -> {
                val interest = commonInterests.random()
                icebreakerTemplates.random()
                    .replace("{interest}", interest)
                    .replace("{dayTime}", dayTimes.random())
                    .replace("{appName}", "SwipeMaster 3000")
            }
            user.interests.isNotEmpty() && target.interests.isNotEmpty() -> {
                val userInterest = user.interests.random()
                val targetInterest = target.interests.random()
                icebreakerTemplates.random()
                    .replace("{interest1}", userInterest)
                    .replace("{interest2}", targetInterest)
                    .replace("{dayTime}", dayTimes.random())
            }
            !user.bio.isNullOrEmpty() -> {
                "I really enjoyed reading your bio! ${generateBioComment(user.bio)}"
            }
            else -> genericIcebreakers.random()
        }
    }

    private fun generateBioComment(bio: String): String {
        return when {
            bio.contains("travel", ignoreCase = true) -> "Your travel experiences sound amazing!"
            bio.contains("food", ignoreCase = true) -> "Your taste in food sounds delicious!"
            bio.contains("music", ignoreCase = true) -> "Your music taste is great!"
            bio.contains("adventure", ignoreCase = true) -> "Your adventurous spirit is inspiring!"
            else -> "It tells me you're an interesting person!"
        }
    }

    private fun generateSmartResponse(lastMessage: String, conversation: List<Message>): String {
        val responses = mapOf(
            "how are you" to listOf(
                "I'm doing great! Thanks for asking. How about you?",
                "Pretty good! Just [current activity]. How's your day going?",
                "Doing well! Can't complain. What's new with you?"
            ),
            "what do you do" to listOf(
                "I work as a [profession]. How about you? What keeps you busy?",
                "I'm in [industry]. What about you? What's your passion?"
            ),
            "hobby" to listOf(
                "I love [hobby1] and [hobby2]! What about you? Any fun hobbies?",
                "Recently I've been getting into [hobby]. Do you have any interests you're passionate about?"
            )
        )

        // Find the best matching response
        for ((key, possibleResponses) in responses) {
            if (lastMessage.contains(key, ignoreCase = true)) {
                return possibleResponses.random()
            }
        }

        // Default engaging responses
        return listOf(
            "That's really interesting! Tell me more about that.",
            "I'd love to hear more about your perspective on that.",
            "That sounds amazing! How did you get into that?",
            "Fascinating! What inspired you to pursue that?",
            "That's cool! What's the story behind that?"
        ).random()
    }

    private fun calculateEngagementLevel(messages: List<Message>): Double {
        if (messages.size < 3) return 0.3

        val user1Messages = messages.count { it.senderId == messages.first().senderId }
        val user2Messages = messages.count { it.senderId != messages.first().senderId }
        val balance = minOf(user1Messages, user2Messages) / maxOf(user1Messages, user2Messages).toDouble()

        val responseTimes = calculateAverageResponseTime(messages)
        val responseScore = 1.0 - (responseTimes / 3600000.0) // Normalize to hours

        return (balance * 0.6 + responseScore * 0.4).coerceIn(0.0, 1.0)
    }

    private fun calculateAverageResponseTime(messages: List<Message>): Long {
        if (messages.size < 2) return 0
        val responseTimes = mutableListOf<Long>()

        for (i in 1 until messages.size) {
            if (messages[i].senderId != messages[i-1].senderId) {
                responseTimes.add(messages[i].timestamp.time - messages[i-1].timestamp.time)
            }
        }

        return if (responseTimes.isNotEmpty()) responseTimes.average().toLong() else 0
    }

    private fun generateSuggestedTopics(messages: List<Message>): List<String> {
        val mentionedTopics = mutableSetOf<String>()
        val commonTopics = listOf("travel", "food", "music", "movies", "hobbies", "work", "family", "goals")

        messages.forEach { message ->
            commonTopics.forEach { topic ->
                if (message.content.contains(topic, ignoreCase = true)) {
                    mentionedTopics.add(topic)
                }
            }
        }

        return (commonTopics - mentionedTopics).take(3)
    }

    private fun detectConversationStyle(messages: List<Message>): String {
        val avgMessageLength = messages.map { it.content.length }.average()
        val questionCount = messages.count { it.content.contains("?") }

        return when {
            avgMessageLength > 100 -> "Detailed"
            questionCount > messages.size * 0.3 -> "Inquisitive"
            avgMessageLength < 30 -> "Concise"
            else -> "Balanced"
        }
    }

    private fun detectRedFlags(messages: List<Message>): List<String> {
        val redFlags = mutableListOf<String>()
        val inappropriateWords = listOf("hot", "sexy", "body", "naked") // Simplified for demo

        messages.forEach { message ->
            inappropriateWords.forEach { word ->
                if (message.content.contains(word, ignoreCase = true)) {
                    redFlags.add("Potentially inappropriate language detected")
                }
            }

            if (message.content.length > 500) {
                redFlags.add("Very long messages might indicate overwhelming behavior")
            }
        }

        return redFlags.distinct()
    }

    private fun calculateCompatibility(user1: User, user2: User): Double {
        var score = 0.0

        // Interest compatibility (40%)
        val interestScore = calculateInterestsCompatibility(user1.interests, user2.interests)
        score += interestScore * 0.4

        // Bio similarity (20%)
        val bioScore = calculateBioSimilarity(user1.bio, user2.bio)
        score += bioScore * 0.2

        // Age compatibility (20%)
        val ageDiff = abs(user1.age - user2.age)
        val ageScore = when {
            ageDiff <= 2 -> 1.0
            ageDiff <= 5 -> 0.7
            ageDiff <= 10 -> 0.4
            else -> 0.1
        }
        score += ageScore * 0.2

        // Location compatibility (20%)
        val locationScore = if (user1.location == user2.location) 1.0 else 0.5
        score += locationScore * 0.2

        return score.coerceIn(0.0, 1.0)
    }

    private fun calculateInterestsCompatibility(interests1: List<String>, interests2: List<String>): Double {
        if (interests1.isEmpty() || interests2.isEmpty()) return 0.5

        val common = interests1.intersect(interests2.toSet()).size
        val total = interests1.union(interests2).size

        return common.toDouble() / total
    }

    private fun calculateBioSimilarity(bio1: String?, bio2: String?): Double {
        if (bio1.isNullOrEmpty() || bio2.isNullOrEmpty()) return 0.3

        val words1 = bio1.split(" ").toSet()
        val words2 = bio2.split(" ").toSet()
        val commonWords = words1.intersect(words2).size

        return commonWords.toDouble() / maxOf(words1.size, words2.size)
    }

    private fun calculateConversationPotential(user1: User, user2: User): Double {
        // Simple heuristic based on bio length and interests
        val bioLengthScore = minOf(1.0, (user1.bio?.length ?: 0 + user2.bio?.length ?: 0) / 200.0)
        val interestsScore = minOf(1.0, (user1.interests.size + user2.interests.size) / 10.0)

        return (bioLengthScore * 0.6 + interestsScore * 0.4)
    }

    private fun calculateLifestyleAlignment(user1: User, user2: User): Double {
        // Simplified lifestyle alignment calculation
        var score = 0.5 // Base score

        // Age proximity bonus
        val ageDiff = abs(user1.age - user2.age)
        if (ageDiff <= 5) score += 0.2

        // Location bonus
        if (user1.location == user2.location) score += 0.3

        return score.coerceIn(0.0, 1.0)
    }

    private fun generateCompatibilityStrengths(user1: User, user2: User): List<String> {
        val strengths = mutableListOf<String>()

        // Common interests
        val commonInterests = user1.interests.intersect(user2.interests.toSet())
        if (commonInterests.isNotEmpty()) {
            strengths.add("Shared interests: ${commonInterests.take(2).joinToString(", ")}")
        }

        // Age compatibility
        val ageDiff = abs(user1.age - user2.age)
        if (ageDiff <= 3) {
            strengths.add("Similar life stage")
        }

        // Location
        if (user1.location == user2.location) {
            strengths.add("Live in the same area")
        }

        // Bio indicators
        if (!user1.bio.isNullOrEmpty() && !user2.bio.isNullOrEmpty()) {
            if (user1.bio.contains("travel") && user2.bio.contains("travel")) {
                strengths.add("Both love traveling")
            }
        }

        return strengths.ifEmpty { listOf("Good potential for interesting conversations") }
    }

    private fun generateCompatibilityConsiderations(user1: User, user2: User): List<String> {
        val considerations = mutableListOf<String>()

        // Age gap
        val ageDiff = abs(user1.age - user2.age)
        if (ageDiff > 10) {
            considerations.add("Significant age difference")
        }

        // Interest diversity
        if (user1.interests.intersect(user2.interests.toSet()).isEmpty()) {
            considerations.add("Different interests - could be complementary or challenging")
        }

        return considerations
    }
}

// Data classes for AI analysis
data class ConversationAnalysis(
    val engagementLevel: Double,
    val suggestedTopics: List<String>,
    val conversationStyle: String,
    val redFlags: List<String>
)

data class CompatibilityScore(
    val overallScore: Double,
    val interestsMatch: Double,
    val conversationPotential: Double,
    val lifestyleAlignment: Double,
    val strengths: List<String>,
    val considerations: List<String>
)

Step 2: AI-Powered Chat Features

Let's enhance our chat with AI features:

// ui/chat/ChatFragment.kt (Add these methods)
class ChatFragment : Fragment() {

    // ... existing code ...

    private fun setupAIFeatures() {
        binding.aiSuggestionsButton.setOnClickListener {
            showAISuggestions()
        }

        // Auto-analyze conversation when it grows
        viewModel.messages.observe(viewLifecycleOwner) { messages ->
            if (messages.size == 5 || messages.size == 10) { // Analyze at milestones
                analyzeConversation(messages)
            }
        }
    }

    private fun showAISuggestions() {
        val messages = viewModel.messages.value ?: emptyList()

        if (messages.isEmpty()) {
            generateIcebreaker()
        } else {
            showResponseSuggestions(messages)
        }
    }

    private fun generateIcebreaker() {
        viewModel.generateIcebreaker().observe(viewLifecycleOwner) { result ->
            result.onSuccess { icebreaker ->
                showIcebreakerSuggestions(icebreaker)
            }.onFailure {
                Snackbar.make(binding.root, "Failed to generate icebreaker", Snackbar.LENGTH_SHORT).show()
            }
        }
    }

    private fun showIcebreakerSuggestions(icebreaker: String) {
        val suggestions = listOf(icebreaker) + generateAlternativeIcebreakers()

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Icebreaker Suggestions")
            .setItems(suggestions.toTypedArray()) { _, which ->
                binding.messageInput.setText(suggestions[which])
                binding.messageInput.requestFocus()
                // Move cursor to end
                binding.messageInput.setSelection(binding.messageInput.text?.length ?: 0)
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun showResponseSuggestions(messages: List<Message>) {
        viewModel.suggestResponse(messages).observe(viewLifecycleOwner) { result ->
            result.onSuccess { suggestions ->
                showResponseOptions(suggestions)
            }.onFailure {
                Snackbar.make(binding.root, "Failed to generate suggestions", Snackbar.LENGTH_SHORT).show()
            }
        }
    }

    private fun showResponseOptions(suggestions: List<String>) {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Response Suggestions")
            .setItems(suggestions.toTypedArray()) { _, which ->
                binding.messageInput.setText(suggestions[which])
                binding.messageInput.requestFocus()
                binding.messageInput.setSelection(binding.messageInput.text?.length ?: 0)
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun analyzeConversation(messages: List<Message>) {
        if (messages.size < 3) return

        viewModel.analyzeConversation(messages).observe(viewLifecycleOwner) { analysis ->
            showConversationInsights(analysis)
        }
    }

    private fun showConversationInsights(analysis: ConversationAnalysis) {
        val insights = buildString {
            append("💡 Conversation Insights\n\n")
            append("Engagement Level: ${(analysis.engagementLevel * 100).toInt()}%\n")
            append("Style: ${analysis.conversationStyle}\n\n")

            if (analysis.suggestedTopics.isNotEmpty()) {
                append("Suggested Topics:\n")
                analysis.suggestedTopics.forEach { topic ->
                    append("• $topic\n")
                }
                append("\n")
            }

            if (analysis.redFlags.isNotEmpty()) {
                append("⚠️ Things to watch for:\n")
                analysis.redFlags.forEach { flag ->
                    append("• $flag\n")
                }
            }
        }

        if (analysis.engagementLevel < 0.3) {
            insights.append("\n💡 Tip: Try asking more open-ended questions!")
        }

        MaterialAlertDialogBuilder(requireContext())
            .setTitle("Conversation Analysis")
            .setMessage(insights)
            .setPositiveButton("Got it") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    private fun generateAlternativeIcebreakers(): List<String> {
        return listOf(
            "I noticed we matched! What's something you're passionate about?",
            "Hey! I'd love to hear what makes you tick. What are you into?",
            "Hi there! Your profile looks interesting. What's your story?",
            "Hello! I'm curious to learn more about you. What are you looking for on here?"
        )
    }
}

// Enhanced ChatViewModel with AI features
@HiltViewModel
class ChatViewModel @Inject constructor(
    private val chatRepository: ChatRepository,
    private val aiService: AIService,
    private val userRepository: UserRepository
) : ViewModel() {

    // ... existing code ...

    fun generateIcebreaker(): LiveData<Result<String>> {
        val result = MutableLiveData<Result<String>>()

        viewModelScope.launch {
            try {
                val currentUser = userRepository.getCurrentUserProfile()
                val otherUser = _otherUser.value ?: return@launch

                val icebreaker = aiService.generateIcebreaker(currentUser, otherUser)
                result.value = icebreaker
            } catch (e: Exception) {
                result.value = Result.failure(e)
            }
        }

        return result
    }

    fun suggestResponse(messages: List<Message>): LiveData<Result<List<String>>> {
        val result = MutableLiveData<Result<List<String>>>()

        viewModelScope.launch {
            try {
                val suggestion = aiService.suggestResponse(messages)
                val alternatives = generateAlternativeResponses(messages.last().content)
                val allSuggestions = listOf(suggestion.getOrThrow()) + alternatives
                result.value = Result.success(allSuggestions)
            } catch (e: Exception) {
                result.value = Result.failure(e)
            }
        }

        return result
    }

    fun analyzeConversation(messages: List<Message>): LiveData<ConversationAnalysis> {
        val result = MutableLiveData<ConversationAnalysis>()

        viewModelScope.launch {
            try {
                val analysis = aiService.analyzeConversation(messages)
                result.value = analysis
            } catch (e: Exception) {
                // Return basic analysis on error
                result.value = ConversationAnalysis(
                    engagementLevel = 0.5,
                    suggestedTopics = emptyList(),
                    conversationStyle = "Unknown",
                    redFlags = emptyList()
                )
            }
        }

        return result
    }

    private fun generateAlternativeResponses(lastMessage: String): List<String> {
        return when {
            lastMessage.contains("?", ignoreCase = true) -> listOf(
                "That's a great question! Let me think...",
                "I'd love to answer that!",
                "Interesting question! Here's my take..."
            )
            lastMessage.length < 20 -> listOf(
                "Tell me more about that!",
                "That sounds interesting! What's the story behind it?",
                "I'd love to hear more!"
            )
            else -> listOf(
                "That's really cool!",
                "I appreciate you sharing that with me.",
                "Thanks for telling me about that!"
            )
        }
    }
}

Chapter 4.2: Premium Subscriptions - "Because Love Shouldn't Be Free"

Step 1: Subscription Data Models

// data/Subscription.kt
data class Subscription(
    val id: String,
    val name: String,
    val description: String,
    val price: Double,
    val billingPeriod: BillingPeriod,
    val features: List<PremiumFeature>,
    val isPopular: Boolean = false,
    val discount: Double? = null
) {
    fun getFormattedPrice(): String {
        return when (billingPeriod) {
            BillingPeriod.MONTHLY -> "$${"%.2f".format(price)}/month"
            BillingPeriod.QUARTERLY -> "$${"%.2f".format(price)}/3 months"
            BillingPeriod.YEARLY -> "$${"%.2f".format(price)}/year"
        }
    }

    fun getSavings(): String? {
        return discount?.let { "Save ${(it * 100).toInt()}%" }
    }
}

enum class BillingPeriod {
    MONTHLY, QUARTERLY, YEARLY
}

enum class PremiumFeature(
    val title: String,
    val description: String,
    val icon: Int
) {
    UNLIMITED_LIKES(
        "Unlimited Likes",
        "Swipe right as much as you want",
        R.drawable.ic_unlimited_likes
    ),
    SEE_WHO_LIKES_YOU(
        "See Who Likes You",
        "Find out who already likes you",
        R.drawable.ic_see_likes
    ),
    REWIND(
        "Rewind",
        "Go back if you accidentally swiped left",
        R.drawable.ic_rewind
    ),
    BOOST(
        "Boost",
        "Be one of the top profiles in your area for 30 minutes",
        R.drawable.ic_boost
    ),
    SUPER_LIKES(
        "5 Super Likes per week",
        "Stand out with Super Likes",
        R.drawable.ic_super_like
    ),
    ADVANCED_FILTERS(
        "Advanced Filters",
        "Filter by height, education, religion and more",
        R.drawable.ic_advanced_filters
    ),
    MESSAGE_PRIORITY(
        "Message Priority",
        "Your messages appear at the top of their inbox",
        R.drawable.ic_priority
    ),
    READ_RECEIPTS(
        "Read Receipts",
        "See when your messages are read",
        R.drawable.ic_read_receipts
    ),
    INCognito_MODE(
        "Incognito Mode",
        "Browse profiles privately",
        R.drawable.ic_incognito
    )
}

data class UserSubscription(
    val subscriptionId: String,
    val purchaseDate: Date,
    val expiryDate: Date,
    val isActive: Boolean,
    val autoRenew: Boolean,
    val features: List<PremiumFeature>
) {
    fun daysRemaining(): Int {
        return maxOf(0, (expiryDate.time - Date().time) / (1000 * 60 * 60 * 24)).toInt()
    }

    fun isExpiringSoon(): Boolean {
        return daysRemaining() <= 7
    }
}

Step 2: Subscription Fragment

Create a beautiful subscription screen:

// ui/subscription/SubscriptionFragment.kt
@AndroidEntryPoint
class SubscriptionFragment : Fragment() {

    private var _binding: FragmentSubscriptionBinding? = null
    private val binding get() = _binding!!

    private val viewModel: SubscriptionViewModel by viewModels()
    private lateinit var subscriptionsAdapter: SubscriptionsAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSubscriptionBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupToolbar()
        setupAdapters()
        setupClickListeners()
        setupObservers()
        loadSubscriptions()

        Log.d("SubscriptionFragment", "User is considering paying for love")
    }

    private fun setupToolbar() {
        binding.toolbar.setNavigationOnClickListener {
            findNavController().navigateUp()
        }
    }

    private fun setupAdapters() {
        subscriptionsAdapter = SubscriptionsAdapter { subscription ->
            showSubscriptionDetails(subscription)
        }

        binding.subscriptionsRecyclerView.apply {
            adapter = subscriptionsAdapter
            layoutManager = LinearLayoutManager(requireContext())
        }
    }

    private fun setupClickListeners() {
        binding.restorePurchasesButton.setOnClickListener {
            viewModel.restorePurchases()
        }

        binding.termsButton.setOnClickListener {
            openTermsAndConditions()
        }

        binding.privacyButton.setOnClickListener {
            openPrivacyPolicy()
        }
    }

    private fun setupObservers() {
        viewModel.subscriptions.observe(viewLifecycleOwner) { subscriptions ->
            subscriptionsAdapter.submitList(subscriptions)
            updateFreeTierInfo()
        }

        viewModel.currentSubscription.observe(viewLifecycleOwner) { subscription ->
            updateCurrentSubscriptionUI(subscription)
        }

        viewModel.purchaseResult.observe(viewLifecycleOwner) { result ->
            handlePurchaseResult(result)
        }

        viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
            binding.loadingIndicator.visibility = if (isLoading) View.VISIBLE else View.GONE
        }
    }

    private fun loadSubscriptions() {
        viewModel.loadSubscriptions()
    }

    private fun updateFreeTierInfo() {
        val freeFeatures = listOf(
            "✓ 100 likes per day",
            "✓ Basic matching",
            "✓ Send messages to matches",
            "✓ Standard filters"
        )

        binding.freeFeaturesText.text = freeFeatures.joinToString("\n")
    }

    private fun updateCurrentSubscriptionUI(subscription: UserSubscription?) {
        if (subscription == null) {
            binding.currentSubscriptionCard.visibility = View.GONE
            binding.upgradeTitle.text = "Upgrade to Premium"
            binding.upgradeSubtitle.text = "Get more matches and better features"
        } else {
            binding.currentSubscriptionCard.visibility = View.VISIBLE
            binding.subscriptionName.text = "Premium Member"
            binding.subscriptionStatus.text = if (subscription.isActive) {
                "Active - ${subscription.daysRemaining()} days remaining"
            } else {
                "Expired"
            }
            binding.manageSubscriptionButton.visibility = if (subscription.isActive) View.VISIBLE else View.GONE

            if (subscription.isExpiringSoon()) {
                binding.expiryWarning.visibility = View.VISIBLE
                binding.expiryWarning.text = "Your subscription expires in ${subscription.daysRemaining()} days"
            } else {
                binding.expiryWarning.visibility = View.GONE
            }
        }
    }

    private fun showSubscriptionDetails(subscription: Subscription) {
        val dialogView = LayoutInflater.from(requireContext())
            .inflate(R.layout.dialog_subscription_details, null)

        val binding = DialogSubscriptionDetailsBinding.bind(dialogView)

        binding.subscriptionName.text = subscription.name
        binding.subscriptionPrice.text = subscription.getFormattedPrice()
        binding.subscriptionDescription.text = subscription.description

        // Show savings if available
        subscription.getSavings()?.let { savings ->
            binding.savingsText.text = savings
            binding.savingsText.visibility = View.VISIBLE
        } ?: run {
            binding.savingsText.visibility = View.GONE
        }

        // Show popular badge
        binding.popularBadge.visibility = if (subscription.isPopular) View.VISIBLE else View.GONE

        // List features
        val featuresText = subscription.features.joinToString("\n") { feature ->
            "✓ ${feature.title}"
        }
        binding.featuresText.text = featuresText

        val dialog = MaterialAlertDialogBuilder(requireContext())
            .setView(dialogView)
            .setPositiveButton("Subscribe") { dialog, _ ->
                viewModel.purchaseSubscription(subscription)
                dialog.dismiss()
            }
            .setNegativeButton("Cancel") { dialog, _ ->
                dialog.dismiss()
            }
            .create()

        dialog.show()
    }

    private fun handlePurchaseResult(result: PurchaseResult) {
        when (result) {
            is PurchaseResult.Success -> {
                Snackbar.make(binding.root, "Subscription activated successfully!", Snackbar.LENGTH_LONG).show()
                findNavController().navigateUp()
            }
            is PurchaseResult.Error -> {
                Snackbar.make(binding.root, "Purchase failed: ${result.message}", Snackbar.LENGTH_LONG).show()
            }
            is PurchaseResult.Cancelled -> {
                // User cancelled, no action needed
            }
        }
    }

    private fun openTermsAndConditions() {
        // Open terms in a WebView or browser
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://yourapp.com/terms"))
        startActivity(intent)
    }

    private fun openPrivacyPolicy() {
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://yourapp.com/privacy"))
        startActivity(intent)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

// Subscription Adapter
class SubscriptionsAdapter(
    private val onSubscriptionClick: (Subscription) -> Unit
) : ListAdapter<Subscription, SubscriptionsAdapter.SubscriptionViewHolder>(SubscriptionDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
        val binding = ItemSubscriptionBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return SubscriptionViewHolder(binding)
    }

    override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
        val subscription = getItem(position)
        holder.bind(subscription)
    }

    inner class SubscriptionViewHolder(
        private val binding: ItemSubscriptionBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val subscription = getItem(position)
                    onSubscriptionClick(subscription)
                }
            }
        }

        fun bind(subscription: Subscription) {
            binding.subscriptionName.text = subscription.name
            binding.subscriptionPrice.text = subscription.getFormattedPrice()
            binding.subscriptionDescription.text = subscription.description

            // Show popular badge
            binding.popularBadge.visibility = if (subscription.isPopular) View.VISIBLE else View.GONE

            // Show savings
            subscription.getSavings()?.let { savings ->
                binding.savingsText.text = savings
                binding.savingsText.visibility = View.VISIBLE
            } ?: run {
                binding.savingsText.visibility = View.GONE
            }

            // Highlight popular subscription
            if (subscription.isPopular) {
                binding.root.strokeWidth = 2
                binding.root.strokeColor = ContextCompat.getColor(binding.root.context, R.color.purple_500)
            } else {
                binding.root.strokeWidth = 0
            }
        }
    }

    class SubscriptionDiffCallback : DiffUtil.ItemCallback<Subscription>() {
        override fun areItemsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
            return oldItem == newItem
        }
    }
}

// Purchase Result Sealed Class
sealed class PurchaseResult {
    object Success : PurchaseResult()
    data class Error(val message: String) : PurchaseResult()
    object Cancelled : PurchaseResult()
}

Step 3: In-App Purchase Integration

// service/BillingService.kt
interface BillingService {
    suspend fun getAvailableSubscriptions(): List<Subscription>
    suspend fun purchaseSubscription(subscription: Subscription): PurchaseResult
    suspend fun getCurrentSubscription(): UserSubscription?
    suspend fun restorePurchases(): Boolean
    suspend fun updateSubscriptionStatus()
}

// Mock implementation (in reality, use Google Play Billing Library)
class MockBillingService @Inject constructor(
    private val preferences: SharedPreferences
) : BillingService {

    private val mockSubscriptions = listOf(
        Subscription(
            id = "premium_monthly",
            name = "Premium Monthly",
            description = "Full access to all premium features",
            price = 9.99,
            billingPeriod = BillingPeriod.MONTHLY,
            features = PremiumFeature.entries
        ),
        Subscription(
            id = "premium_quarterly",
            name = "Premium Quarterly",
            description = "Full access to all premium features",
            price = 24.99,
            billingPeriod = BillingPeriod.QUARTERLY,
            features = PremiumFeature.entries,
            discount = 0.17 // 17% discount
        ),
        Subscription(
            id = "premium_yearly",
            name = "Premium Yearly",
            description = "Full access to all premium features",
            price = 79.99,
            billingPeriod = BillingPeriod.YEARLY,
            features = PremiumFeature.entries,
            isPopular = true,
            discount = 0.33 // 33% discount
        )
    )

    override suspend fun getAvailableSubscriptions(): List<Subscription> {
        delay(500) // Simulate network delay
        return mockSubscriptions
    }

    override suspend fun purchaseSubscription(subscription: Subscription): PurchaseResult {
        delay(1000) // Simulate purchase process

        return try {
            // Simulate 90% success rate
            if (Random.nextDouble() < 0.9) {
                // Save purchase
                val expiryDate = Calendar.getInstance().apply {
                    when (subscription.billingPeriod) {
                        BillingPeriod.MONTHLY -> add(Calendar.MONTH, 1)
                        BillingPeriod.QUARTERLY -> add(Calendar.MONTH, 3)
                        BillingPeriod.YEARLY -> add(Calendar.YEAR, 1)
                    }
                }.time

                val userSubscription = UserSubscription(
                    subscriptionId = subscription.id,
                    purchaseDate = Date(),
                    expiryDate = expiryDate,
                    isActive = true,
                    autoRenew = true,
                    features = subscription.features
                )

                saveSubscription(userSubscription)
                PurchaseResult.Success
            } else {
                PurchaseResult.Error("Payment processing failed")
            }
        } catch (e: Exception) {
            PurchaseResult.Error(e.message ?: "Unknown error")
        }
    }

    override suspend fun getCurrentSubscription(): UserSubscription? {
        return getSavedSubscription()
    }

    override suspend fun restorePurchases(): Boolean {
        delay(800)
        // In reality, this would contact the Play Store to restore purchases
        return getSavedSubscription() != null
    }

    override suspend fun updateSubscriptionStatus() {
        getSavedSubscription()?.let { subscription ->
            if (subscription.expiryDate.before(Date())) {
                // Subscription expired
                saveSubscription(subscription.copy(isActive = false))
            }
        }
    }

    private fun saveSubscription(subscription: UserSubscription) {
        val gson = Gson()
        preferences.edit()
            .putString("user_subscription", gson.toJson(subscription))
            .apply()
    }

    private fun getSavedSubscription(): UserSubscription? {
        val json = preferences.getString("user_subscription", null)
        return if (json != null) {
            val gson = Gson()
            gson.fromJson(json, UserSubscription::class.java)
        } else {
            null
        }
    }
}

Chapter 4.3: Analytics & Performance - "Because We're Nosy"

Step 1: Analytics Service

// service/AnalyticsService.kt
interface AnalyticsService {
    fun trackEvent(event: AnalyticsEvent)
    fun trackScreenView(screenName: String)
    fun trackUserProperty(property: String, value: Any)
    fun trackError(error: Throwable, context: String)
    fun trackPurchase(subscription: Subscription)
}

class FirebaseAnalyticsService @Inject constructor(
    private val firebaseAnalytics: FirebaseAnalytics
) : AnalyticsService {

    override fun trackEvent(event: AnalyticsEvent) {
        val bundle = Bundle().apply {
            event.properties.forEach { (key, value) ->
                when (value) {
                    is String -> putString(key, value)
                    is Int -> putInt(key, value)
                    is Long -> putLong(key, value)
                    is Double -> putDouble(key, value)
                    is Boolean -> putBoolean(key, value)
                    else -> putString(key, value.toString())
                }
            }
        }

        firebaseAnalytics.logEvent(event.name, bundle)
        Log.d("Analytics", "Tracked event: ${event.name} with properties: ${event.properties}")
    }

    override fun trackScreenView(screenName: String) {
        firebaseAnalytics.setCurrentScreen(requireActivity(), screenName, null)
        Log.d("Analytics", "Screen view: $screenName")
    }

    override fun trackUserProperty(property: String, value: Any) {
        firebaseAnalytics.setUserProperty(property, value.toString())
        Log.d("Analytics", "User property: $property = $value")
    }

    override fun trackError(error: Throwable, context: String) {
        val bundle = Bundle().apply {
            putString("error_message", error.message)
            putString("error_context", context)
            putString("stack_trace", error.stackTraceToString())
        }

        firebaseAnalytics.logEvent("app_error", bundle)
        Log.e("Analytics", "Error tracked: $context - ${error.message}")
    }

    override fun trackPurchase(subscription: Subscription) {
        val bundle = Bundle().apply {
            putString("subscription_id", subscription.id)
            putString("subscription_name", subscription.name)
            putDouble("price", subscription.price)
            putString("billing_period", subscription.billingPeriod.name)
        }

        firebaseAnalytics.logEvent("purchase", bundle)
        Log.d("Analytics", "Purchase tracked: ${subscription.name}")
    }
}

// Analytics Events
sealed class AnalyticsEvent(
    val name: String,
    val properties: Map<String, Any> = emptyMap()
) {
    // App Events
    object AppOpen : AnalyticsEvent("app_open")
    object AppBackground : AnalyticsEvent("app_background")

    // Authentication Events
    object UserSignUp : AnalyticsEvent("user_signup")
    object UserLogin : AnalyticsEvent("user_login")
    object UserLogout : AnalyticsEvent("user_logout")

    // Profile Events
    data class ProfileView(val userId: String) : AnalyticsEvent("profile_view", mapOf("user_id" to userId))
    data class ProfileEdit(val field: String) : AnalyticsEvent("profile_edit", mapOf("field" to field))
    data class PhotoUpload(val count: Int) : AnalyticsEvent("photo_upload", mapOf("photo_count" to count))

    // Swipe Events
    object SwipeSessionStart : AnalyticsEvent("swipe_session_start")
    object SwipeSessionEnd : AnalyticsEvent("swipe_session_end")
    data class Swipe(val direction: String, val userId: String) : 
        AnalyticsEvent("swipe", mapOf("direction" to direction, "user_id" to userId))
    data class SuperLike(val userId: String) : AnalyticsEvent("super_like", mapOf("user_id" to userId))

    // Match Events
    data class Match(val matchId: String) : AnalyticsEvent("match", mapOf("match_id" to matchId))
    data class Unmatch(val matchId: String) : AnalyticsEvent("unmatch", mapOf("match_id" to matchId))

    // Message Events
    data class MessageSent(val matchId: String, val length: Int) : 
        AnalyticsEvent("message_sent", mapOf("match_id" to matchId, "message_length" to length))
    data class MessageRead(val matchId: String, val messageId: String) : 
        AnalyticsEvent("message_read", mapOf("match_id" to matchId, "message_id" to messageId))

    // Subscription Events
    data class SubscriptionView(val subscriptionId: String) : 
        AnalyticsEvent("subscription_view", mapOf("subscription_id" to subscriptionId))
    data class SubscriptionPurchase(val subscriptionId: String, val price: Double) : 
        AnalyticsEvent("subscription_purchase", mapOf("subscription_id" to subscriptionId, "price" to price))

    // Feature Events
    data class FeatureUse(val feature: String) : AnalyticsEvent("feature_use", mapOf("feature" to feature))
    data class AISuggestionUse(val context: String) : 
        AnalyticsEvent("ai_suggestion_use", mapOf("context" to context))

    // Error Events
    data class Error(val context: String, val error: String) : 
        AnalyticsEvent("error", mapOf("context" to context, "error" to error))
}

Step 2: Performance Monitoring

// service/PerformanceService.kt
class PerformanceService @Inject constructor() {

    private val startupTime = mutableMapOf<String, Long>()
    private val screenLoadTimes = mutableMapOf<String, Long>()

    fun trackAppStart() {
        startupTime["app_start"] = System.currentTimeMillis()
    }

    fun trackAppReady() {
        startupTime["app_ready"] = System.currentTimeMillis()
        val loadTime = startupTime["app_ready"]!! - startupTime["app_start"]!!
        Log.d("Performance", "App startup time: ${loadTime}ms")

        // Track in analytics if desired
        if (loadTime > 2000) {
            Log.w("Performance", "Slow app startup detected: ${loadTime}ms")
        }
    }

    fun trackScreenLoadStart(screenName: String) {
        screenLoadTimes[screenName] = System.currentTimeMillis()
    }

    fun trackScreenLoadEnd(screenName: String) {
        val startTime = screenLoadTimes[screenName] ?: return
        val loadTime = System.currentTimeMillis() - startTime
        Log.d("Performance", "Screen $screenName load time: ${loadTime}ms")

        if (loadTime > 1000) {
            Log.w("Performance", "Slow screen load detected for $screenName: ${loadTime}ms")
        }
    }

    fun trackApiCall(apiName: String, duration: Long, success: Boolean) {
        Log.d("Performance", "API $apiName call: ${duration}ms, success: $success")

        if (duration > 5000) {
            Log.w("Performance", "Slow API call detected: $apiName took ${duration}ms")
        }
    }

    fun trackImageLoad(url: String, duration: Long, success: Boolean) {
        if (duration > 1000) {
            Log.w("Performance", "Slow image load: $url took ${duration}ms")
        }
    }
}

What We've Built - A World-Class Dating App!

🎉 HOLY ENTERPRISE FEATURES, BATMAN! We've just transformed our dating app into a sophisticated platform that could compete with the biggest players in the market!

Advanced Features Added:

  1. AI-Powered Conversations: Smart icebreakers, response suggestions, and conversation analysis
  2. Premium Subscriptions: Multiple subscription tiers with in-app purchases
  3. Advanced Analytics: Comprehensive tracking of user behavior and app performance
  4. Performance Monitoring: Real-time performance tracking and optimization
  5. Enterprise Architecture: Scalable, maintainable codebase with proper separation of concerns

Production-Ready Features:

Joke Break - Because Even Enterprise Apps Need Humor:

Why did the AI break up with the analytics? It felt like it was being watched too closely!

What's a programmer's favorite subscription feature? The one that automatically filters out people who don't use version control!

Why was the performance monitoring so good? Because it knew all the right metrics to track!

Your dating app now has everything needed for success:

The app is now truly enterprise-ready and could be scaled to millions of users! You've built a complete, professional dating platform that combines cutting-edge technology with solid business fundamentals.

Congratulations! You've just built what could be the next big dating app! 🚀💕

Final thoughts: Remember to test thoroughly, gather user feedback, and continuously iterate based on data. The dating app market is competitive, but with these features and solid execution, you're well-positioned for success!

Back to ChameleonSoftwareOnline.com