Build a Mood-Expense Analytics Engine

Tag expenses with detected moods and visualize spending patterns with charts.

Introduction

30 Second Summary

Ever noticed you spend more on takeout after a stressful day, or splurge online when you're feeling low? Your emotions have a bigger impact on your wallet than you might think.

In this project, you will build a mood detection quiz and analytics dashboard that reveals how emotions drive your spending patterns. Your Android expense tracker will ask 3 quick questions, score your answers with a weighted algorithm, and visualize monthly mood-spending breakdowns through charts.

What You'll Build

You log an expense, answer 3 quick mood questions, and the app tags that purchase with your detected emotional state. At the end of the month, you open the analytics dashboard and instantly see which moods are costing you the most.

By the end of this project, you'll have:

  • A mood detection quiz that randomly selects 3 questions from a 50-question bank, scores your answers across 5 mood categories, and displays the detected mood with a confidence percentage.
  • A mood-tagged expense flow where every purchase you log is automatically paired with the quiz result and stored in your Room database.
  • A monthly analytics dashboard with a pie chart showing spending proportions by mood and a bar chart showing absolute amounts, so you can spot which emotions drive the biggest purchases.
  • Secret Mission: Generate behavioral insight nudges from mood-spending patterns and add a validation feedback loop that measures your algorithm's accuracy over time.

Are there any prerequisites?

This project builds on an existing Android project with a Room database (an Expense entity with id, amount, category, mood, date, and note fields), an authentication system, and MVVM architecture already in place.

You should have Sprints 1-2 of the Mood-Expense project complete, with Android Studio installed and your project compiling successfully.

Before We Start

Before we dive into code, let's lock in what you're about to build and why it matters.

Add MPAndroidChart and Create the Question Bank Entity

Your expense tracker can log spending, but it has no way to detect or store mood data. Before you can build the quiz and analytics dashboard, the app needs two things: a charting library and a question bank.

In this step, you will add MPAndroidChart for data visualization and create a Room entity to hold your 50-question mood bank with weighted scoring data.

In this step, get ready to:
  • Add the MPAndroidChart charting library to your project.
  • Create the MoodQuestion entity and data access layer.
  • Seed the database with 50 mood questions and verify with Database Inspector.
Add the MPAndroidChart dependency

MPAndroidChart is an open-source charting library for Android. You will use it later to render pie charts and bar charts on the analytics dashboard. It is hosted on JitPack, so you need to add that repository first.

  • In Android Studio, open your settings.gradle file (it lives at the project root level, not inside any module folder).
  • Inside the dependencyResolutionManagement > repositories block, add the JitPack repository by pasting this line:
maven { url = uri("https://jitpack.io") }
  • Now open your module-level build.gradle file (the one inside your app/ folder).
  • Inside the dependencies block, add the MPAndroidChart dependency:
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
  • Click Sync Now in the yellow banner that appears at the top of the editor.

Wait for the sync to complete. The status bar at the bottom should show a successful sync with no errors.

Sync failed?

Make sure the maven { url = uri("https://jitpack.io") } line is inside dependencyResolutionManagement > repositories in settings.gradle, not in the project-level build.gradle. Newer Android Studio projects resolve repositories from settings.gradle only.

help me fix my Gradle sync failure after adding JitPack

Create the MoodQuestion entity and DAO

Each question in your bank has 5 answer options. Each option carries weighted scores (1-3 points) across the 5 moods: Happy, Neutral, Sad, Angry, and Stressed. This flat-field structure avoids JSON serialization and keeps Room queries simple.

  • Create a new Kotlin file at data/entity/MoodQuestion.kt by right-clicking the data/entity package in the Project pane, selecting New > Kotlin Class/File, and entering MoodQuestion.
  • Paste the following entity code:
package com.example.moodexpense.data.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "mood_questions")
data class MoodQuestion(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val questionText: String,
    val category: String, // Emotional, Financial, Motivational, External, Lifestyle
    val option1Text: String,
    val option1Happy: Int, val option1Neutral: Int, val option1Sad: Int, val option1Angry: Int, val option1Stressed: Int,
    val option2Text: String,
    val option2Happy: Int, val option2Neutral: Int, val option2Sad: Int, val option2Angry: Int, val option2Stressed: Int,
    val option3Text: String,
    val option3Happy: Int, val option3Neutral: Int, val option3Sad: Int, val option3Angry: Int, val option3Stressed: Int,
    val option4Text: String,
    val option4Happy: Int, val option4Neutral: Int, val option4Sad: Int, val option4Angry: Int, val option4Stressed: Int,
    val option5Text: String,
    val option5Happy: Int, val option5Neutral: Int, val option5Sad: Int, val option5Angry: Int, val option5Stressed: Int
)

What does this entity do?

Each row in the mood_questions table represents one quiz question. The category field groups questions into Emotional, Financial, Motivational, External, and Lifestyle buckets for balanced quiz selection.

The 25 integer fields (5 options x 5 moods) store the weighted points each answer contributes to each mood. This flat structure means Room can query directly without any JSON parsing or TypeConverters.

  • Create a new Kotlin file at data/dao/MoodQuestionDao.kt by right-clicking the data/dao package, selecting New > Kotlin Class/File, and entering MoodQuestionDao.
  • Paste the following DAO interface:
package com.example.moodexpense.data.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.example.moodexpense.data.entity.MoodQuestion

@Dao
interface MoodQuestionDao {

    @Insert
    suspend fun insertAll(questions: List<MoodQuestion>)

    @Query("SELECT * FROM mood_questions")
    suspend fun getAllQuestions(): List<MoodQuestion>

    @Query("SELECT * FROM mood_questions WHERE category = :category ORDER BY RANDOM() LIMIT 1")
    suspend fun getRandomQuestionByCategory(category: String): MoodQuestion?

    @Query("SELECT COUNT(*) FROM mood_questions")
    suspend fun getQuestionCount(): Int
}

What do these queries do?

  • insertAll bulk-inserts the entire question bank in one transaction.
  • getRandomQuestionByCategory picks one random question from a specific category. The quiz will call this once per selected category for balanced representation.
  • getQuestionCount lets you verify the seeder ran correctly.
  • Open your existing AppDatabase.kt file.
  • Add MoodQuestion::class to the entities array in the @Database annotation.
  • Increment the version number by one (for example, from 2 to 3).
  • Add an abstract function that exposes the new DAO:
// Add this inside your AppDatabase class body
abstract fun moodQuestionDao(): MoodQuestionDao
  • Save MoodQuestion.kt, MoodQuestionDao.kt, and AppDatabase.kt.
  • Build the project by pressing Cmd+F9 (macOS) or Ctrl+F9 (Windows). Confirm the build succeeds with no errors in the Build output panel.

Build errors about schema changes?

If you see an error about Room schema changes, make sure you incremented the version number in the @Database annotation. Also add .fallbackToDestructiveMigration() to your database builder if you do not have a migration strategy yet. This clears the old database on version change during development.

help me fix Room database schema migration errors

Seed the question bank and verify

The question bank needs to be populated exactly once, the first time the database is created. Room provides a RoomDatabase.Callback that fires on onCreate, which is the perfect hook for seeding.

  • Create a new Kotlin file called MoodQuestionSeeder.kt in your data package.
  • Add the seeder class structure with your 50 questions distributed across the 5 categories (10 per category):
package com.example.moodexpense.data

import com.example.moodexpense.data.entity.MoodQuestion

class MoodQuestionSeeder {

    fun getQuestions(): List<MoodQuestion> {
        return listOf(
            // Emotional category (10 questions)
            MoodQuestion(
                questionText = "How would you describe your energy level right now?",
                category = "Emotional",
                option1Text = "Energised and excited",
                option1Happy = 3, option1Neutral = 1, option1Sad = 0, option1Angry = 0, option1Stressed = 0,
                option2Text = "Calm and balanced",
                option2Happy = 1, option2Neutral = 3, option2Sad = 0, option2Angry = 0, option2Stressed = 0,
                option3Text = "Low and drained",
                option3Happy = 0, option3Neutral = 1, option3Sad = 3, option3Angry = 0, option3Stressed = 1,
                option4Text = "Frustrated and tense",
                option4Happy = 0, option4Neutral = 0, option4Sad = 1, option4Angry = 3, option4Stressed = 2,
                option5Text = "Overwhelmed and anxious",
                option5Happy = 0, option5Neutral = 0, option5Sad = 1, option5Angry = 1, option5Stressed = 3
            ),
            // Add your remaining 49 questions here
            // 10 per category: Emotional, Financial, Motivational, External, Lifestyle
        )
    }
}

How should I write my 50 questions?

Each question needs 5 answer options. Each option assigns weighted points (0-3) to the 5 mood categories. The weights determine how strongly an answer signals a particular mood.

A weight of 3 means strong signal, 2 means moderate, 1 means weak, and 0 means no signal. Distribute 10 questions across each of the 5 categories: Emotional, Financial, Motivational, External, and Lifestyle.

  • Complete your seeder with all 50 questions. Make sure each category has exactly 10 questions for balanced selection.
  • Now wire the seeder to your database. Open AppDatabase.kt and add a RoomDatabase.Callback to your database builder:
// Add this callback to your Room.databaseBuilder chain
.addCallback(object : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        // Seed mood questions on first database creation
        CoroutineScope(Dispatchers.IO).launch {
            val dao = getInstance(context).moodQuestionDao()
            val seeder = MoodQuestionSeeder()
            dao.insertAll(seeder.getQuestions())
        }
    }
})

What does this callback do?

The onCreate callback fires only once, the very first time Room creates the database file. It launches a coroutine on Dispatchers.IO to insert all 50 questions without blocking the main thread.

If you have already run the app before this change, uninstall the app from your emulator first. The callback only fires on fresh database creation, not on version upgrades.

  • Save AppDatabase.kt and MoodQuestionSeeder.kt.
  • Uninstall the app from your emulator or device to ensure a fresh database creation. You can do this by long-pressing the app icon and selecting Uninstall.

Before you run the app, what do you expect to see when you check the database? Think about how many rows the mood_questions table should contain.

  • Run the app by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
  • Once the app launches on the emulator, open Database Inspector by clicking View in the menu bar, then Tool Windows, then App Inspection.
  • Select the Database Inspector tab at the top of the panel.
  • Select your running app process from the dropdown.
  • Expand your database node in the left pane and double-click the mood_questions table.

You should see 50 rows in the table, each with a question text, a category value (Emotional, Financial, Motivational, External, or Lifestyle), and populated weight columns for all 5 options.

Table is empty or missing?

If the mood_questions table is empty, the onCreate callback did not fire. This happens when the database already existed from a previous run. Uninstall the app completely from the emulator and run again.

If the table does not appear at all, confirm you added MoodQuestion::class to the entities array in the @Database annotation and incremented the version number.

help me debug my Room database seeder not populating data

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files: MoodQuestion.kt, MoodQuestionDao.kt, MoodQuestionSeeder.kt, and AppDatabase.kt.

ⓧ I'd like to double check the full code

Confirm each file matches the following. First, MoodQuestion.kt:

package com.example.moodexpense.data.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "mood_questions")
data class MoodQuestion(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val questionText: String,
    val category: String, // Emotional, Financial, Motivational, External, Lifestyle
    val option1Text: String,
    val option1Happy: Int, val option1Neutral: Int, val option1Sad: Int, val option1Angry: Int, val option1Stressed: Int,
    val option2Text: String,
    val option2Happy: Int, val option2Neutral: Int, val option2Sad: Int, val option2Angry: Int, val option2Stressed: Int,
    val option3Text: String,
    val option3Happy: Int, val option3Neutral: Int, val option3Sad: Int, val option3Angry: Int, val option3Stressed: Int,
    val option4Text: String,
    val option4Happy: Int, val option4Neutral: Int, val option4Sad: Int, val option4Angry: Int, val option4Stressed: Int,
    val option5Text: String,
    val option5Happy: Int, val option5Neutral: Int, val option5Sad: Int, val option5Angry: Int, val option5Stressed: Int
)

Next, MoodQuestionDao.kt:

package com.example.moodexpense.data.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.example.moodexpense.data.entity.MoodQuestion

@Dao
interface MoodQuestionDao {

    @Insert
    suspend fun insertAll(questions: List<MoodQuestion>)

    @Query("SELECT * FROM mood_questions")
    suspend fun getAllQuestions(): List<MoodQuestion>

    @Query("SELECT * FROM mood_questions WHERE category = :category ORDER BY RANDOM() LIMIT 1")
    suspend fun getRandomQuestionByCategory(category: String): MoodQuestion?

    @Query("SELECT COUNT(*) FROM mood_questions")
    suspend fun getQuestionCount(): Int
}

For AppDatabase.kt, confirm you have MoodQuestion::class in the entities array, an incremented version number, the abstract fun moodQuestionDao(): MoodQuestionDao function, and the RoomDatabase.Callback with the seeder wired in onCreate.

For MoodQuestionSeeder.kt, confirm it returns a list of exactly 50 MoodQuestion objects, 10 per category (Emotional, Financial, Motivational, External, Lifestyle), each with all 5 answer options and their mood weights populated.

Your data layer is ready. Next up, you will build the scoring engine that takes a learner's quiz answers and calculates a detected mood with a confidence percentage.

Build the Mood Quiz Screen with Scoring Engine

Your database holds 50 mood questions, but there is no way for users to see them or for the app to produce a mood from their answers.

This step builds the two missing pieces: a scoring engine that turns weighted answers into a detected mood with a confidence percentage, and a quiz UI that presents 3 random questions and runs the algorithm. This is the academic core of your dissertation's mood detection system.

In this step, get ready to:
  • Create the weighted scoring engine that calculates mood from quiz answers.
  • Build the quiz Fragment UI with one-at-a-time question display.
  • Test the full quiz flow and manually verify the scoring math.
Create the scoring engine

The scoring engine takes a list of answered questions and sums the weighted points each answer contributes to the 5 mood categories. It then picks the highest-scoring mood and calculates confidence.

  • Create a new package called mood inside com.example.moodexpense.
  • Inside that package, create a new file called MoodResult.kt with this data class:
package com.example.moodexpense.mood

data class MoodResult(
    val detectedMood: String,
    val confidence: Double,
    val scores: Map<String, Int>
)

What does this code do?

This data class holds the output of the scoring algorithm.

  • detectedMood stores the winning mood category (e.g. "Stressed").
  • confidence is the percentage certainty, calculated as the highest score divided by total score.
  • scores is the full breakdown so you can display all 5 mood scores.
  • In the same mood package, create MoodScoringEngine.kt.
  • Start with the AnsweredQuestion data class and the engine's class declaration with the scores map:
package com.example.moodexpense.mood

import com.example.moodexpense.data.entity.MoodQuestion

data class AnsweredQuestion(
    val question: MoodQuestion,
    val selectedOption: Int // 1-5
)

class MoodScoringEngine {

    fun calculateMood(answers: List<AnsweredQuestion>): MoodResult {
        val scores = mutableMapOf(
            "Happy" to 0,
            "Neutral" to 0,
            "Sad" to 0,
            "Angry" to 0,
            "Stressed" to 0
        )

What does this code do?

  • AnsweredQuestion pairs a question with the option number (1-5) the user selected.
  • The scores map initialises all 5 mood categories to zero before tallying begins.
  • Below the scores map, add the loop that iterates over each answered question and tallies weighted points for options 1 and 2:
        for (answer in answers) {
            val q = answer.question
            when (answer.selectedOption) {
                1 -> {
                    scores["Happy"] = scores["Happy"]!! + q.option1Happy
                    scores["Neutral"] = scores["Neutral"]!! + q.option1Neutral
                    scores["Sad"] = scores["Sad"]!! + q.option1Sad
                    scores["Angry"] = scores["Angry"]!! + q.option1Angry
                    scores["Stressed"] = scores["Stressed"]!! + q.option1Stressed
                }
                2 -> {
                    scores["Happy"] = scores["Happy"]!! + q.option2Happy
                    scores["Neutral"] = scores["Neutral"]!! + q.option2Neutral
                    scores["Sad"] = scores["Sad"]!! + q.option2Sad
                    scores["Angry"] = scores["Angry"]!! + q.option2Angry
                    scores["Stressed"] = scores["Stressed"]!! + q.option2Stressed
                }

What's happening here?

Each answered question carries preset weights for all 5 moods. The when block checks which option the user picked and adds that option's mood weights to the running totals.

For example, if option 1 of a question has option1Stressed = 3 and option1Happy = 0, choosing that option pushes the Stressed tally higher without affecting Happy.

  • Continue the when block by adding cases 3, 4, and 5 following the same pattern:
                3 -> {
                    scores["Happy"] = scores["Happy"]!! + q.option3Happy
                    scores["Neutral"] = scores["Neutral"]!! + q.option3Neutral
                    scores["Sad"] = scores["Sad"]!! + q.option3Sad
                    scores["Angry"] = scores["Angry"]!! + q.option3Angry
                    scores["Stressed"] = scores["Stressed"]!! + q.option3Stressed
                }
                4 -> {
                    scores["Happy"] = scores["Happy"]!! + q.option4Happy
                    scores["Neutral"] = scores["Neutral"]!! + q.option4Neutral
                    scores["Sad"] = scores["Sad"]!! + q.option4Sad
                    scores["Angry"] = scores["Angry"]!! + q.option4Angry
                    scores["Stressed"] = scores["Stressed"]!! + q.option4Stressed
                }
                5 -> {
                    scores["Happy"] = scores["Happy"]!! + q.option5Happy
                    scores["Neutral"] = scores["Neutral"]!! + q.option5Neutral
                    scores["Sad"] = scores["Sad"]!! + q.option5Sad
                    scores["Angry"] = scores["Angry"]!! + q.option5Angry
                    scores["Stressed"] = scores["Stressed"]!! + q.option5Stressed
                }
  • Close the when and for blocks, then add the final calculation that determines the detected mood and confidence:
            }
        }

        val highestScore = scores.values.max()
        val totalScore = scores.values.sum()
        val detectedMood = scores.entries.first { it.value == highestScore }.key
        val confidence = if (totalScore > 0) (highestScore.toDouble() / totalScore) * 100 else 0.0

        return MoodResult(
            detectedMood = detectedMood,
            confidence = Math.round(confidence * 10.0) / 10.0,
            scores = scores
        )
    }
}

How does the confidence formula work?

Confidence equals the highest mood score divided by the total of all scores, multiplied by 100. If one mood dominates (e.g. 9 out of 12 total points), confidence is high (75%).

If scores are evenly spread (e.g. all moods near 2-3 points each), confidence drops toward 20%, signalling weak detection. The result is rounded to one decimal place.

  • Save both MoodResult.kt and MoodScoringEngine.kt.
  • Build the project (Build > Make Project). It should compile without errors, confirming the scoring engine is syntactically correct.

Seeing a compile error?

  • Check that the package declaration in both files is com.example.moodexpense.mood and the import in MoodScoringEngine.kt points to com.example.moodexpense.data.entity.MoodQuestion.
  • Ensure every when branch has matching opening and closing braces.
  • help me fix a compile error in my MoodScoringEngine
Build the quiz Fragment UI

The scoring engine is ready but has no user-facing entry point. You need a Fragment that loads 3 random questions, shows them one at a time, collects answers, and feeds them into the engine.

  • Create a new layout file called fragment_mood_quiz.xml in your res/layout folder.
  • Add the top section with the question number, question text, and radio group:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp">

    <TextView
        android:id="@+id/tvQuestionNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Question 1 of 3"
        android:textSize="14sp"
        android:textColor="@android:color/darker_gray" />

    <TextView
        android:id="@+id/tvQuestionText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text=""
        android:textSize="18sp"
        android:textStyle="bold" />

    <RadioGroup
        android:id="@+id/radioGroupOptions"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp" />

What does this layout do?

  • tvQuestionNumber shows progress ("Question 1 of 3").
  • tvQuestionText displays the current question in bold.
  • radioGroupOptions holds the 5 answer options (populated dynamically in Kotlin).
  • Below the RadioGroup, add the Next button and the hidden result text view, then close the layout:
    <Button
        android:id="@+id/btnNext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Next" />

    <TextView
        android:id="@+id/tvResult"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:textSize="16sp"
        android:visibility="gone" />

</LinearLayout>

What does this code do?

  • btnNext advances to the next question or triggers scoring after the final answer.
  • tvResult starts hidden (visibility="gone") and appears only after the quiz is complete to show the detected mood.
  • Save fragment_mood_quiz.xml.
  • Create a new package ui.quiz inside com.example.moodexpense.
  • Inside it, create MoodQuizFragment.kt. Start with the imports, class declaration, and properties:
package com.example.moodexpense.ui.quiz

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.example.moodexpense.R
import com.example.moodexpense.data.AppDatabase
import com.example.moodexpense.data.entity.MoodQuestion
import com.example.moodexpense.mood.AnsweredQuestion
import com.example.moodexpense.mood.MoodResult
import com.example.moodexpense.mood.MoodScoringEngine
import kotlinx.coroutines.launch

class MoodQuizFragment : Fragment() {

    private lateinit var tvQuestionNumber: TextView
    private lateinit var tvQuestionText: TextView
    private lateinit var radioGroup: RadioGroup
    private lateinit var btnNext: Button
    private lateinit var tvResult: TextView
  • Add the state properties and onCreateView directly below:
    private val categories = listOf("Emotional", "Financial", "Motivational", "External", "Lifestyle")
    private var questions: List<MoodQuestion> = emptyList()
    private var currentIndex = 0
    private val answers = mutableListOf<AnsweredQuestion>()
    private val scoringEngine = MoodScoringEngine()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_mood_quiz, container, false)
    }

What do these properties track?

  • categories holds all 5 question categories. The fragment shuffles them and picks 3 for balanced selection.
  • currentIndex tracks which question the user is on (0, 1, or 2).
  • answers collects each answered question to pass to the scoring engine at the end.
  • Add onViewCreated which wires up the views and the Next button click handler:
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        tvQuestionNumber = view.findViewById(R.id.tvQuestionNumber)
        tvQuestionText = view.findViewById(R.id.tvQuestionText)
        radioGroup = view.findViewById(R.id.radioGroupOptions)
        btnNext = view.findViewById(R.id.btnNext)
        tvResult = view.findViewById(R.id.tvResult)

        loadQuestions()

        btnNext.setOnClickListener {
            val selectedId = radioGroup.checkedRadioButtonId
            if (selectedId == -1) return@setOnClickListener

            val selectedOption = radioGroup.indexOfChild(view.findViewById(selectedId)) + 1
            answers.add(AnsweredQuestion(questions[currentIndex], selectedOption))

            currentIndex++
            if (currentIndex < questions.size) {
                displayQuestion()
            } else {
                showResult()
            }
        }
    }

What does this code do?

The click listener first guards against no selection (checkedRadioButtonId == -1). It then records the selected option number and advances.

After the 3rd answer, showResult() is called instead of loading the next question.

  • Add the loadQuestions function that fetches one random question from each of 3 randomly-selected categories:
    private fun loadQuestions() {
        lifecycleScope.launch {
            val db = AppDatabase.getInstance(requireContext())
            val selectedCategories = categories.shuffled().take(3)
            val loadedQuestions = mutableListOf<MoodQuestion>()

            for (category in selectedCategories) {
                val question = db.moodQuestionDao().getRandomQuestionByCategory(category)
                if (question != null) {
                    loadedQuestions.add(question)
                }
            }

            questions = loadedQuestions
            if (questions.isNotEmpty()) {
                displayQuestion()
            }
        }
    }

Why shuffle and take 3?

Shuffling the 5 categories and taking 3 ensures balanced representation without always picking the same categories. Each quiz round draws from a different mix, strengthening the academic validity of your mood detection.

  • Add the displayQuestion function that populates the UI with the current question's text and options:
    private fun displayQuestion() {
        val question = questions[currentIndex]
        tvQuestionNumber.text = "Question ${currentIndex + 1} of ${questions.size}"
        tvQuestionText.text = question.questionText

        radioGroup.removeAllViews()
        val options = listOf(
            question.option1Text, question.option2Text, question.option3Text,
            question.option4Text, question.option5Text
        )
        for (optionText in options) {
            val radioButton = RadioButton(requireContext())
            radioButton.text = optionText
            radioButton.id = View.generateViewId()
            radioGroup.addView(radioButton)
        }

        btnNext.text = if (currentIndex == questions.size - 1) "See Result" else "Next"
        tvResult.visibility = View.GONE
    }

What does this code do?

  • It clears previous radio buttons with removeAllViews() then dynamically creates 5 new RadioButton views from the question's option text fields.
  • The button label changes to "See Result" on the final question so the user knows they are about to get their mood result.
  • Add the showResult and getMoodResult functions, then close the class:
    private fun showResult() {
        val result: MoodResult = scoringEngine.calculateMood(answers)

        tvQuestionNumber.visibility = View.GONE
        tvQuestionText.visibility = View.GONE
        radioGroup.visibility = View.GONE
        btnNext.visibility = View.GONE

        tvResult.visibility = View.VISIBLE
        tvResult.text = "Detected Mood: ${result.detectedMood}\nConfidence: ${result.confidence}%\n\nScores:\n" +
                result.scores.entries.joinToString("\n") { "${it.key}: ${it.value}" }
    }

    fun getMoodResult(): MoodResult? {
        return if (answers.size == questions.size && questions.isNotEmpty()) {
            scoringEngine.calculateMood(answers)
        } else null
    }
}

What does this code do?

  • showResult() hides all quiz UI and displays the detected mood, confidence percentage, and full score breakdown.
  • getMoodResult() is a public accessor that the expense entry flow will call in a later step to retrieve the mood tag.
  • Save MoodQuizFragment.kt.
  • Build the project (Build > Make Project). The build should succeed with no errors.

Build fails with unresolved reference?

  • Verify R.layout.fragment_mood_quiz matches your XML filename exactly (fragment_mood_quiz.xml).
  • Confirm your view IDs in the XML (tvQuestionNumber, tvQuestionText, radioGroupOptions, btnNext, tvResult) match the findViewById calls in the Fragment.
  • help me fix unresolved references in my MoodQuizFragment

✔️ Awesome, I've got everything!

Great. Make sure you have saved all files before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm you have each of these pieces in order:

  • MoodResult.kt with the MoodResult data class (3 fields).
  • MoodScoringEngine.kt with AnsweredQuestion data class and calculateMood function (scores map, when block for options 1-5, final calculation).
  • fragment_mood_quiz.xml with tvQuestionNumber, tvQuestionText, radioGroupOptions, btnNext, tvResult.
  • MoodQuizFragment.kt with loadQuestions, displayQuestion, showResult, and getMoodResult functions.
Test with hardcoded answers and verify scoring

The quiz compiles but you have not yet seen it run. Now you will navigate to the quiz, answer 3 questions, and manually verify that the scoring output matches your expected calculation.

  • Add a temporary navigation entry point to the quiz from your app's main screen (e.g. a button that launches MoodQuizFragment via your Navigation component or a FragmentTransaction).
  • Run the app on your emulator or device.
  • Tap the button to open the mood quiz.

Before you answer the questions, think about this: if you pick options that carry high Stressed weights, what mood do you expect the engine to output?

  • Answer all 3 questions by selecting an option for each and tapping Next (or See Result on the final question).

The result screen shows "Detected Mood: [mood]" followed by the confidence percentage and a breakdown of all 5 scores.

  • Manually verify the math: look up the weight values for the options you chose in your question bank (use Database Inspector if needed). Sum the mood weights across your 3 answers and confirm the engine's highest mood and confidence match.

How to manually verify

Say you chose option 2 for question A (Stressed weight: 3), option 1 for question B (Stressed weight: 2), and option 4 for question C (Stressed weight: 1). Your total Stressed score should be 6.

If the total across all moods is 15, confidence is (6/15)*100 = 40%. The result screen should say "Detected Mood: Stressed" with "Confidence: 40.0%".

Quiz shows no questions or crashes?

  • Open Database Inspector and confirm the mood_questions table still has 50 rows. If the database was wiped during reinstall, your seeder should repopulate on the next launch.
  • Check Logcat for Room query errors. The getRandomQuestionByCategory query returns null if no rows match the category string exactly (case-sensitive).
  • help me debug why my mood quiz shows no questions

Your mood detection algorithm is working end-to-end. Next up, you will wire this quiz into the expense entry flow so every logged expense gets tagged with the detected mood.

Wire Expense Entry to the Mood Quiz

Your mood quiz works perfectly in isolation. It selects 3 questions, scores the answers, and displays a detected mood with a confidence percentage.

The problem is that the quiz has no connection to your expense entry flow. Without this wiring, mood tags never reach the database and you cannot build the analytics dashboard later. This step closes the loop: the learner logs an expense, takes the quiz, and the mood-tagged expense lands in Room.

In this step, get ready to:
  • Redirect expense entry to launch the mood quiz instead of saving directly.
  • Add a Confirm & Save button that stores the mood-tagged expense in Room.
  • Verify the full flow end-to-end using Database Inspector.
Modify expense entry flow to trigger the quiz

Right now, your expense entry screen saves the expense directly when the user taps Save. You need to intercept that tap so it launches the MoodQuizFragment instead, passing the expense data along as arguments.

  • Open ExpenseEntryFragment.kt in Android Studio.
  • Find your Save button click listener where the expense is currently inserted into Room.
  • Replace the direct database insert with navigation to the quiz, passing amount, category, and note as arguments. Add this code inside your Save button's click listener:
// Navigate to quiz instead of saving directly
val amount = etAmount.text.toString().toDoubleOrNull() ?: return@setOnClickListener
val category = spinnerCategory.selectedItem.toString()
val note = etNote.text.toString()

// Bundle expense data to pass to the quiz fragment
val bundle = Bundle().apply {
    putDouble("expense_amount", amount)
    putString("expense_category", category)
    putString("expense_note", note)
}

// Launch the mood quiz with expense data attached
findNavController().navigate(
    R.id.action_expenseEntry_to_moodQuiz,
    bundle
)

What does this code do?

Instead of creating an Expense object and inserting it into Room right away, the Save button now bundles the expense data (amount, category, note) and navigates to the mood quiz fragment.

The quiz will receive this bundle, run the mood detection, and then handle the actual database save after the learner confirms.

  • Make sure you have a navigation action defined in your nav graph that connects your expense entry destination to the mood quiz destination. The action ID should be action_expenseEntry_to_moodQuiz.
  • Save ExpenseEntryFragment.kt.

Run the app and tap Save on the expense entry screen. You should now see the mood quiz screen appear instead of the expense being saved immediately.

App crashes when tapping Save?

  • Check that your navigation graph has the action action_expenseEntry_to_moodQuiz defined. A missing action causes an IllegalArgumentException.
  • Confirm your expense entry fragment is using the Navigation component. If you are using FragmentTransaction instead, replace findNavController().navigate() with a FragmentTransaction that passes the bundle as arguments.

Why is my navigation crashing when I tap Save?

Store the mood-tagged expense after quiz completion

The quiz now launches from the expense entry screen. But after it shows the detected mood, there is no way for the learner to confirm and save. You need a button that takes the quiz result and the original expense data, combines them into a full Expense object, and writes it to Room.

  • Open fragment_mood_quiz.xml and add a Confirm & Save button below the result TextView:
<Button
    android:id="@+id/btnConfirmSave"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:text="Confirm &amp; Save"
    android:visibility="gone" />

What does this code do?

This adds a button with ID btnConfirmSave that starts hidden (visibility="gone"). It only appears after the quiz finishes and shows the detected mood. The learner sees their mood result first, then decides to confirm the save.

  • Save fragment_mood_quiz.xml.

Now you need to update MoodQuizFragment.kt to receive the expense data from the bundle and wire up the Confirm & Save button.

  • Open MoodQuizFragment.kt. Add these properties at the top of the class, alongside your existing property declarations:
// Expense data passed from the entry screen
private var expenseAmount: Double = 0.0
private var expenseCategory: String = ""
private var expenseNote: String = ""
private lateinit var btnConfirmSave: Button

What does this code do?

These properties store the expense data that was bundled from the entry screen. The fragment will read these from its arguments when it starts up.

  • Inside onViewCreated, after your existing view bindings, add code to retrieve the expense arguments and bind the Confirm & Save button:
// Retrieve expense data from arguments
expenseAmount = arguments?.getDouble("expense_amount") ?: 0.0
expenseCategory = arguments?.getString("expense_category") ?: ""
expenseNote = arguments?.getString("expense_note") ?: ""

// Bind the confirm button
btnConfirmSave = view.findViewById(R.id.btnConfirmSave)

What does this code do?

The fragment pulls the expense amount, category, and note from the navigation arguments. These are the same values the expense entry screen bundled before navigating here.

  • Now find the showResult() method in your fragment. Add this code at the end of that method, after tvResult.text = ...:
// Show the confirm button and wire the save logic
btnConfirmSave.visibility = View.VISIBLE
btnConfirmSave.setOnClickListener {
    lifecycleScope.launch {
        val db = AppDatabase.getInstance(requireContext())
        val expense = Expense(
            amount = expenseAmount,
            category = expenseCategory,
            mood = result.detectedMood,
            date = System.currentTimeMillis(),
            note = expenseNote
        )
        db.expenseDao().insert(expense)

        // Navigate back with success message
        Toast.makeText(requireContext(),
            "Expense saved with mood: ${result.detectedMood}",
            Toast.LENGTH_SHORT).show()
        findNavController().popBackStack()
    }
}

What does this code do?

  • btnConfirmSave.visibility = View.VISIBLE makes the button appear only after the quiz result is displayed.
  • The click listener creates an Expense object that combines the original entry data (amount, category, note) with the quiz result (detectedMood).
  • After inserting into Room, a Toast confirms the save and popBackStack() returns the learner to the main screen.
  • Make sure you have the necessary imports at the top of MoodQuizFragment.kt: Expense entity, Toast, and findNavController.
  • Save MoodQuizFragment.kt.

Seeing an unresolved reference error on Expense?

  • Confirm you have imported your Expense entity class at the top of the file (e.g. import com.example.moodexpense.data.entity.Expense).
  • If your Expense entity lives in a different package path, adjust the import to match your project structure.

Help me fix unresolved reference errors in my MoodQuizFragment.

Verify the full expense-to-mood flow

You have wired every piece together. Time to test the complete loop from expense entry through quiz to database storage.

  • Run the app on your emulator or device.
  • Navigate to the expense entry screen.
  • Enter 25.00 for the amount.
  • Select Food as the category.
  • Type lunch in the note field.
  • Tap Save.

The mood quiz screen should appear. Answer all 3 questions.

  • After seeing the detected mood and confidence, tap Confirm & Save.

Before you check the database, what do you expect to see in the mood column for this expense?

  • Open Database Inspector in Android Studio: click View in the top menu bar.
  • Select Tool Windows, then App Inspection.
  • Select the Database Inspector tab and open your expenses table.

You should see a row with amount=25.0, category="Food", mood set to whatever mood the quiz detected (e.g. "Stressed" or "Happy"), and the note "lunch". The mood field is now populated from the quiz result.

Mood column still null in the database?

  • Make sure the result variable in showResult() is accessible inside the btnConfirmSave click listener. If result is declared as a local variable, the listener can still capture it via closure.
  • Confirm you are passing result.detectedMood to the Expense constructor's mood parameter, not an empty string.
  • Check that your Expense entity's mood field is a nullable String (or has a default). If it is non-null and you accidentally pass null, Room will throw an exception before inserting.

Help me debug why the mood is not being saved to my expense.

The core loop is now complete. Every expense the learner saves goes through the mood quiz and lands in Room with mood data attached. Next up, you will build a filterable expense history so you can browse mood-tagged entries without opening Database Inspector.

Build Expense History with Mood Filtering

You have mood-tagged expenses sitting in the database, but the only way to confirm they exist is by opening Database Inspector. That is fine for development, but your users need a proper list they can browse and filter.

In this step, you will build a RecyclerView-based expense history screen. Each list item displays the date, amount, category, mood, and note. A row of filter chips at the top lets the user narrow the list to a single mood, making emotional spending patterns visible at a glance before you build the charts in the next step.

In this step, get ready to:
  • Build a RecyclerView displaying all mood-tagged expenses.
  • Add mood filter chips and a month selector to narrow the list.
  • Verify filtering works with multiple test expenses.
Build the history RecyclerView

Before you can display expenses in a list, your ExpenseDao needs queries that filter by mood and by date range. You will add three new queries, then build the list item layout and the fragment.

  • Add the following filter queries to your ExpenseDao.kt file, below your existing insert and query methods:
@Query("SELECT * FROM expenses WHERE mood = :mood ORDER BY date DESC")
suspend fun getExpensesByMood(mood: String): List<Expense>

@Query("SELECT * FROM expenses WHERE date BETWEEN :start AND :end ORDER BY date DESC")
suspend fun getExpensesBetweenDates(start: Long, end: Long): List<Expense>

@Query("SELECT * FROM expenses WHERE mood = :mood AND date BETWEEN :start AND :end ORDER BY date DESC")
suspend fun getExpensesByMoodAndDateRange(mood: String, start: Long, end: Long): List<Expense>

What do these queries do?

Each query targets a different filter scenario. getExpensesByMood returns only expenses matching a specific mood string.

getExpensesBetweenDates returns expenses within a date range (stored as epoch milliseconds). getExpensesByMoodAndDateRange combines both filters for the full mood-plus-month scenario.

  • Save ExpenseDao.kt.
  • Confirm the project still builds by pressing Ctrl+F9 (Windows) or Cmd+F9 (macOS) to rebuild. A successful build with no errors confirms your queries are syntactically correct.

Seeing a build error in ExpenseDao?

Check that each @Query annotation string is on a single line with no accidental line breaks inside the SQL. Also confirm the parameter names (:mood, :start, :end) match the function arguments exactly.

Help me debug my Room DAO query error.

Now you need a layout for each row in the list.

  • Create a new layout file item_expense.xml inside res/layout/ by running the following code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="12dp">

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

        <TextView
            android:id="@+id/tvExpenseDate"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="12sp"
            android:textColor="@android:color/darker_gray" />

        <TextView
            android:id="@+id/tvExpenseAmount"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textStyle="bold" />
    </LinearLayout>

</LinearLayout>

What does this layout do?

This first section creates a horizontal row displaying the expense date on the left and the amount on the right. The layout_weight pushes the amount to the far right.

  • Add the remaining views below the first LinearLayout (inside the root), to show category, mood chip, and note:
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/tvExpenseCategory"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp" />

        <TextView
            android:id="@+id/tvExpenseMood"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:paddingHorizontal="8dp"
            android:paddingVertical="2dp"
            android:textSize="12sp"
            android:textColor="@android:color/white"
            android:background="@android:color/darker_gray" />
    </LinearLayout>

    <TextView
        android:id="@+id/tvExpenseNote"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:textSize="12sp"
        android:textColor="@android:color/darker_gray" />

What does this add?

The tvExpenseCategory shows the spending category (Food, Transport, etc.). The tvExpenseMood acts as a colour-coded chip displaying the detected mood. The tvExpenseNote shows any note the user typed when logging the expense.

  • Save item_expense.xml.
  • Open the Design tab in the layout editor to confirm you see the date, amount, category, mood badge, and note stacked vertically.

Next, create the fragment layout that holds the filter bar and the RecyclerView.

  • Create fragment_expense_history.xml inside res/layout/ with the following content:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <HorizontalScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:id="@+id/chipContainer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal" />
    </HorizontalScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:orientation="horizontal"
        android:gravity="center">

        <Button
            android:id="@+id/btnPrevMonth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="&lt;" />
    </LinearLayout>
</LinearLayout>
  • Continue building the layout by adding the month label, next-month button, and RecyclerView below the previous button (inside the same horizontal LinearLayout and then below it):
        <TextView
            android:id="@+id/tvMonth"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="July 2026"
            android:textSize="16sp"
            android:textStyle="bold" />

        <Button
            android:id="@+id/btnNextMonth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="&gt;" />
    </LinearLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerExpenses"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="8dp" />

</LinearLayout>

What does this layout do?

The chipContainer LinearLayout (wrapped in a HorizontalScrollView) will hold the mood filter chips you create programmatically. Below it, previous/next buttons and a month label form the date navigation. The RecyclerView fills the remaining space thanks to layout_weight="1".

  • Save fragment_expense_history.xml.

With the layouts ready, create the Fragment that wires everything together.

  • Create ExpenseHistoryFragment.kt inside ui/history/ with the adapter and initial setup:
package com.example.moodexpense.ui.history

import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.moodexpense.R
import com.example.moodexpense.data.AppDatabase
import com.example.moodexpense.data.entity.Expense
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
  • Below the imports, add the inner adapter class that binds expense data to your list item views:
class ExpenseHistoryFragment : Fragment() {

    // Maps mood names to display colours
    private val moodColors = mapOf(
        "Happy" to Color.parseColor("#4CAF50"),
        "Neutral" to Color.parseColor("#9E9E9E"),
        "Sad" to Color.parseColor("#2196F3"),
        "Angry" to Color.parseColor("#FF9800"),
        "Stressed" to Color.parseColor("#F44336")
    )

    private lateinit var recyclerView: RecyclerView
    private lateinit var chipContainer: LinearLayout
    private lateinit var tvMonth: TextView
    private lateinit var btnPrevMonth: Button
    private lateinit var btnNextMonth: Button

    private val calendar = Calendar.getInstance()
    private var selectedMood: String? = null
    private var expenses = mutableListOf<Expense>()
    private val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())

What are these properties?

moodColors maps each mood string to a hex colour, used to tint the mood badge in each list item. selectedMood tracks which filter chip is active (null means "All"). The Calendar instance tracks the currently displayed month for date filtering.

  • Add the inner ViewHolder and Adapter below the properties:
    // ViewHolder binds each expense to item_expense.xml
    inner class ExpenseViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val tvDate: TextView = view.findViewById(R.id.tvExpenseDate)
        val tvAmount: TextView = view.findViewById(R.id.tvExpenseAmount)
        val tvCategory: TextView = view.findViewById(R.id.tvExpenseCategory)
        val tvMood: TextView = view.findViewById(R.id.tvExpenseMood)
        val tvNote: TextView = view.findViewById(R.id.tvExpenseNote)
    }

    inner class ExpenseAdapter : RecyclerView.Adapter<ExpenseViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpenseViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_expense, parent, false)
            return ExpenseViewHolder(view)
        }

        override fun onBindViewHolder(holder: ExpenseViewHolder, position: Int) {
            val expense = expenses[position]
            holder.tvDate.text = dateFormat.format(Date(expense.date))
            holder.tvAmount.text = "$${expense.amount}"
            holder.tvCategory.text = expense.category
            holder.tvMood.text = expense.mood ?: "Unknown"
            holder.tvMood.setBackgroundColor(moodColors[expense.mood] ?: Color.GRAY)
            holder.tvNote.text = expense.note ?: ""
        }

        override fun getItemCount() = expenses.size
    }

What does the adapter do?

onBindViewHolder fills each list item with the expense's date, amount, category, mood (colour-coded background), and note. The adapter reads directly from the expenses mutable list, so updating that list and calling notifyDataSetChanged() refreshes the UI.

  • Add onCreateView and onViewCreated to inflate the layout and wire up the RecyclerView:
    private val adapter = ExpenseAdapter()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_expense_history, container, false)
    }

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

        recyclerView = view.findViewById(R.id.recyclerExpenses)
        chipContainer = view.findViewById(R.id.chipContainer)
        tvMonth = view.findViewById(R.id.tvMonth)
        btnPrevMonth = view.findViewById(R.id.btnPrevMonth)
        btnNextMonth = view.findViewById(R.id.btnNextMonth)

        recyclerView.layoutManager = LinearLayoutManager(requireContext())
        recyclerView.adapter = adapter

        setupChips()
        setupMonthNavigation()
        loadExpenses()
    }
  • Save ExpenseHistoryFragment.kt.
  • The file will show errors for setupChips(), setupMonthNavigation(), and loadExpenses() because you have not written them yet. You will add those in the next substep.
Add mood and date filters

The RecyclerView structure is in place, but tapping a mood chip needs to actually re-query the database with the selected mood. You will now add the chip creation logic, the month navigation, and the data-loading function that ties everything together.

  • Add the setupChips() function below onViewCreated in ExpenseHistoryFragment.kt:
    private fun setupChips() {
        val moods = listOf("All", "Happy", "Neutral", "Sad", "Angry", "Stressed")

        for (mood in moods) {
            val chip = Button(requireContext())
            chip.text = mood
            chip.setOnClickListener {
                selectedMood = if (mood == "All") null else mood
                loadExpenses()
            }
            chipContainer.addView(chip)
        }
    }

What does setupChips do?

It creates a button for each mood category plus "All". Tapping a chip sets selectedMood to that mood (or null for All) and calls loadExpenses() to re-query the database with the new filter.

  • Add the setupMonthNavigation() function below setupChips():
    private fun setupMonthNavigation() {
        updateMonthLabel()

        btnPrevMonth.setOnClickListener {
            calendar.add(Calendar.MONTH, -1)
            updateMonthLabel()
            loadExpenses()
        }

        btnNextMonth.setOnClickListener {
            calendar.add(Calendar.MONTH, 1)
            updateMonthLabel()
            loadExpenses()
        }
    }

    private fun updateMonthLabel() {
        val monthNames = arrayOf("January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December")
        tvMonth.text = "${monthNames[calendar.get(Calendar.MONTH)]} ${calendar.get(Calendar.YEAR)}"
    }

What does this do?

The previous and next buttons shift the calendar by one month, update the label, and reload expenses for the new month range. updateMonthLabel() renders the current month and year as a readable string.

  • Add the loadExpenses() function that queries the DAO based on the active filters and closes the fragment class:
    private fun loadExpenses() {
        lifecycleScope.launch {
            val db = AppDatabase.getInstance(requireContext())

            // Calculate start and end of the selected month
            val startCal = calendar.clone() as Calendar
            startCal.set(Calendar.DAY_OF_MONTH, 1)
            startCal.set(Calendar.HOUR_OF_DAY, 0)
            startCal.set(Calendar.MINUTE, 0)
            startCal.set(Calendar.SECOND, 0)
            val startDate = startCal.timeInMillis

            val endCal = calendar.clone() as Calendar
            endCal.set(Calendar.DAY_OF_MONTH, endCal.getActualMaximum(Calendar.DAY_OF_MONTH))
            endCal.set(Calendar.HOUR_OF_DAY, 23)
            endCal.set(Calendar.MINUTE, 59)
            endCal.set(Calendar.SECOND, 59)
            val endDate = endCal.timeInMillis

            // Query based on active filters
            val result = if (selectedMood != null) {
                db.expenseDao().getExpensesByMoodAndDateRange(selectedMood!!, startDate, endDate)
            } else {
                db.expenseDao().getExpensesBetweenDates(startDate, endDate)
            }

            expenses.clear()
            expenses.addAll(result)
            adapter.notifyDataSetChanged()
        }
    }
}

What does loadExpenses do?

It calculates the first and last millisecond of the currently displayed month. Then it picks the right DAO query depending on whether a mood filter is active. After fetching the results, it clears the list, adds the new data, and tells the adapter to refresh.

  • Save ExpenseHistoryFragment.kt.
  • Rebuild the project (Ctrl+F9 / Cmd+F9). The build should succeed with no errors.

Build fails with unresolved reference?

Make sure the Expense import at the top of the file points to your existing entity: com.example.moodexpense.data.entity.Expense. Also verify that AppDatabase.getInstance(requireContext()) matches the singleton method name in your existing database class.

Help me fix an unresolved reference error in my ExpenseHistoryFragment.

You now need to make the history screen accessible from your app's navigation. Add the fragment to your navigation graph or wire a button/menu item to navigate to ExpenseHistoryFragment using the same navigation pattern your project already uses.

  • Run the app on your emulator or device.
  • Navigate to the expense history screen. You should see any previously logged expenses listed with their date, amount, category, a coloured mood badge, and note.

Seeing an empty list?

If you do not see any expenses, confirm you have logged at least one mood-tagged expense in the previous step. Also check that the month selector is set to the month you logged expenses in. Tap the previous/next month buttons to navigate to the correct month.

Help me figure out why my expense history list is empty.

Verify filtering works

The list displays expenses, but you need to confirm the mood chips actually filter the data correctly. You will log a few test expenses with different moods and verify the filter narrows the list.

  • Log 3 or more test expenses using your expense entry flow, answering the mood quiz differently each time to get different mood tags (for example, aim for at least one Happy and one Stressed).
  • Navigate to the expense history screen.
  • Confirm you can see all test expenses listed with their mood badges colour-coded.

Before you tap a filter chip, what do you expect will happen to the list when you select "Stressed"?

  • Tap the Stressed filter chip.

Only expenses tagged "Stressed" remain in the list. All other moods disappear.

  • Tap the All chip.
  • Confirm all expenses return to the list.
  • Tap the month navigation arrows to move to a month with no expenses. Confirm the list shows empty.

Tapping a chip does not filter?

Double-check that setOnClickListener in setupChips() calls loadExpenses() after setting selectedMood. Also verify the mood strings in your chip list ("Happy", "Stressed", etc.) match exactly what your quiz stores in the database. A casing mismatch ("stressed" vs "Stressed") will cause the query to return zero results.

Help me debug my mood filter chips.

✔️ Awesome, I've got everything!

Great. Double-check you have saved both layout files and ExpenseHistoryFragment.kt.

ⓧ I'd like to double check the full code

Your ExpenseHistoryFragment.kt is assembled from the chunks above. Scroll up and confirm these sections are present in order:

  • Package declaration and imports (android.graphics.Color, Fragment, RecyclerView, Calendar, etc.).
  • Class declaration with moodColors, view references, selectedMood, expenses list, and dateFormat.
  • Inner ExpenseViewHolder class and ExpenseAdapter class.
  • onCreateView and onViewCreated with RecyclerView setup and calls to setupChips(), setupMonthNavigation(), loadExpenses().
  • setupChips() creating mood filter buttons.
  • setupMonthNavigation() and updateMonthLabel().
  • loadExpenses() with date range calculation and DAO query logic.

Your ExpenseDao.kt should include the three new queries: getExpensesByMood, getExpensesBetweenDates, and getExpensesByMoodAndDateRange.

Your expense history is live and filterable. Next up, you will build the analytics dashboard with pie and bar charts that transform these mood-tagged expenses into visual patterns your users can act on.

Build the Monthly Mood-Spending Analytics Dashboard

Your expense history screen lets you filter by mood, but scrolling through a list does not reveal proportional patterns. A pie chart instantly shows "60% of your spending happened when Stressed" while a bar chart shows the absolute amounts.

In this step you will build a monthly analytics dashboard using MPAndroidChart. This is the visual payoff that turns raw mood-tagged data into at-a-glance spending insights.

In this step, get ready to:
  • Create the analytics layout with a pie chart and bar chart.
  • Build an AnalyticsFragment that queries expenses by month and renders mood-spending data.
  • Add month navigation so you can browse spending patterns across different months.
Create the analytics layout with a pie chart

The analytics screen needs two chart views plus a month navigation bar at the top. You will create the full layout now and then progressively wire it up with data.

  • In your res/layout folder, create a new file called fragment_analytics.xml.
  • Add the top section with the ScrollView wrapper, outer LinearLayout, and the month navigation row by pasting this code:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center">

            <Button
                android:id="@+id/btnPrevMonth"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="&lt;" />

            <TextView
                android:id="@+id/tvMonth"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:text="July 2026"
                android:textSize="18sp"
                android:textStyle="bold" />

What does this layout section do?

The outer ScrollView allows the content to scroll when both charts are taller than the screen. The inner horizontal LinearLayout creates a row with a "previous" button, a month label, and a "next" button for navigating between months.

  • Continue building the layout by adding the next month button, the chart views, and closing tags below the code you just pasted:
            <Button
                android:id="@+id/btnNextMonth"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="&gt;" />
        </LinearLayout>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Spending by Mood"
            android:textSize="16sp"
            android:textStyle="bold" />

        <com.github.mikephil.charting.charts.PieChart
            android:id="@+id/pieChart"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:layout_marginTop="8dp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="Amount Spent per Mood"
            android:textSize="16sp"
            android:textStyle="bold" />

        <com.github.mikephil.charting.charts.BarChart
            android:id="@+id/barChart"
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:layout_marginTop="8dp" />

    </LinearLayout>
</ScrollView>

What are these chart XML elements?

The com.github.mikephil.charting.charts.PieChart and com.github.mikephil.charting.charts.BarChart elements come from the MPAndroidChart library you added in Step 1. They render as empty placeholders until you feed them data from Kotlin code.

  • Save fragment_analytics.xml.

Now create the Kotlin fragment that will populate these charts. Start with the class structure and the colour mapping for each mood.

  • Create a new package ui/analytics and inside it create AnalyticsFragment.kt.
  • Add the package declaration, imports, class declaration, and properties:
package com.example.moodexpense.ui.analytics

import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.example.moodexpense.R
import com.example.moodexpense.data.AppDatabase
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.PieChart
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.IndexAxisValueFormatter
import com.github.mikephil.charting.formatter.PercentFormatter
import com.github.mikephil.charting.components.XAxis
import kotlinx.coroutines.launch
import java.util.Calendar

Why so many chart imports?

MPAndroidChart separates its API into chart views, data containers, data sets, entries, and formatters. Each import pulls in one piece of that pipeline. You will use all of them across the pie chart and bar chart setup.

  • Below the imports, add the class declaration with properties and the onCreateView method:
class AnalyticsFragment : Fragment() {

    private lateinit var pieChart: PieChart
    private lateinit var barChart: BarChart
    private lateinit var tvMonth: TextView
    private lateinit var btnPrevMonth: Button
    private lateinit var btnNextMonth: Button

    private val calendar = Calendar.getInstance()

    // Map each mood to a display colour
    private val moodColors = mapOf(
        "Happy" to Color.parseColor("#4CAF50"),
        "Neutral" to Color.parseColor("#9E9E9E"),
        "Sad" to Color.parseColor("#2196F3"),
        "Angry" to Color.parseColor("#FF9800"),
        "Stressed" to Color.parseColor("#F44336")
    )

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_analytics, container, false)
    }

What does the moodColors map do?

Each mood category maps to a hex colour that stays consistent across both charts. Happy is green, Stressed is red, Sad is blue, Angry is orange, and Neutral is grey. This makes patterns instantly recognisable.

  • Add the onViewCreated method which binds the views and sets up month navigation:
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        pieChart = view.findViewById(R.id.pieChart)
        barChart = view.findViewById(R.id.barChart)
        tvMonth = view.findViewById(R.id.tvMonth)
        btnPrevMonth = view.findViewById(R.id.btnPrevMonth)
        btnNextMonth = view.findViewById(R.id.btnNextMonth)

        btnPrevMonth.setOnClickListener {
            calendar.add(Calendar.MONTH, -1)
            updateMonthLabel()
            loadData()
        }

        btnNextMonth.setOnClickListener {
            calendar.add(Calendar.MONTH, 1)
            updateMonthLabel()
            loadData()
        }

        updateMonthLabel()
        loadData()
    }

What does onViewCreated set up?

It grabs references to the chart views and buttons from the layout. Each month button shifts the calendar by one month, updates the label, and reloads the chart data. On first load, it shows the current month.

  • Add the updateMonthLabel and loadData methods that query your expenses by date range:
    private fun updateMonthLabel() {
        val monthNames = arrayOf("January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December")
        tvMonth.text = "${monthNames[calendar.get(Calendar.MONTH)]} ${calendar.get(Calendar.YEAR)}"
    }

    private fun loadData() {
        lifecycleScope.launch {
            val db = AppDatabase.getInstance(requireContext())

            // Calculate start of month
            val startCal = calendar.clone() as Calendar
            startCal.set(Calendar.DAY_OF_MONTH, 1)
            startCal.set(Calendar.HOUR_OF_DAY, 0)
            startCal.set(Calendar.MINUTE, 0)
            startCal.set(Calendar.SECOND, 0)
            val startDate = startCal.timeInMillis

            // Calculate end of month
            val endCal = calendar.clone() as Calendar
            endCal.set(Calendar.DAY_OF_MONTH, endCal.getActualMaximum(Calendar.DAY_OF_MONTH))
            endCal.set(Calendar.HOUR_OF_DAY, 23)
            endCal.set(Calendar.MINUTE, 59)
            endCal.set(Calendar.SECOND, 59)
            val endDate = endCal.timeInMillis

What does loadData calculate?

It clones the current calendar state to compute the first millisecond of the month (startDate) and the last millisecond (endDate). These boundaries feed into your existing getExpensesBetweenDates DAO query.

  • Complete the loadData method by querying expenses and grouping them by mood:
            // Query expenses for the selected month
            val expenses = db.expenseDao().getExpensesBetweenDates(startDate, endDate)

            // Group spending totals by mood
            val moodTotals = mutableMapOf<String, Double>()
            for (expense in expenses) {
                val mood = expense.mood ?: "Neutral"
                moodTotals[mood] = (moodTotals[mood] ?: 0.0) + expense.amount
            }

            setupPieChart(moodTotals)
            setupBarChart(moodTotals)
        }
    }

What is moodTotals?

A map from mood name to total amount spent under that mood for the selected month. If an expense has no mood tag, it defaults to "Neutral". This single map feeds both charts.

  • Add the setupPieChart method that converts moodTotals into a colour-coded pie chart with percentages:
    private fun setupPieChart(moodTotals: Map<String, Double>) {
        val entries = mutableListOf<PieEntry>()
        val colors = mutableListOf<Int>()

        for ((mood, total) in moodTotals) {
            entries.add(PieEntry(total.toFloat(), mood))
            colors.add(moodColors[mood] ?: Color.GRAY)
        }

        val dataSet = PieDataSet(entries, "Mood Spending")
        dataSet.colors = colors
        dataSet.sliceSpace = 3f
        dataSet.valueTextSize = 12f
        dataSet.valueTextColor = Color.WHITE

        val data = PieData(dataSet)
        data.setValueFormatter(PercentFormatter(pieChart))

        pieChart.data = data
        pieChart.setUsePercentValues(true)
        pieChart.description.isEnabled = false
        pieChart.setDrawHoleEnabled(true)
        pieChart.setHoleColor(Color.WHITE)
        pieChart.setHoleRadius(40f)
        pieChart.animateY(1000)
        pieChart.invalidate()
    }

How does the pie chart show percentages?

The PercentFormatter combined with setUsePercentValues(true) tells MPAndroidChart to display each slice as a percentage of the total rather than the raw amount. The animateY(1000) call adds a smooth one-second build-up animation when the chart first appears.

  • Save AnalyticsFragment.kt.
  • Wire AnalyticsFragment into your navigation graph or activity so you can reach it from the main screen (for example, add a bottom nav item or menu entry pointing to it).
  • Run the app on your emulator.
  • Navigate to the analytics screen.

You should see a pie chart with colour-coded slices representing your mood-tagged test expenses. Each slice displays a percentage label.

Seeing a blank chart?

Make sure you have at least 2 mood-tagged expenses in the current month. If you logged test expenses in a previous month, tap the navigation arrows to find them.

Confirm the getExpensesBetweenDates query in your DAO matches the signature getExpensesBetweenDates(start: Long, end: Long) and returns expenses with the mood field populated.

help me debug why my pie chart is empty in AnalyticsFragment

Add a bar chart showing spending amount per mood

The pie chart shows proportional breakdown, but you also need absolute amounts. A bar chart makes it easy to compare exactly how much you spent under each mood.

  • In AnalyticsFragment.kt, add the setupBarChart method below setupPieChart:
    private fun setupBarChart(moodTotals: Map<String, Double>) {
        val entries = mutableListOf<BarEntry>()
        val labels = mutableListOf<String>()
        val colors = mutableListOf<Int>()

        var index = 0f
        for ((mood, total) in moodTotals) {
            entries.add(BarEntry(index, total.toFloat()))
            labels.add(mood)
            colors.add(moodColors[mood] ?: Color.GRAY)
            index++
        }

        val dataSet = BarDataSet(entries, "Spending Amount")
        dataSet.colors = colors
        dataSet.valueTextSize = 10f

        val data = BarData(dataSet)
        barChart.data = data

        barChart.xAxis.valueFormatter = IndexAxisValueFormatter(labels)
        barChart.xAxis.position = XAxis.XAxisPosition.BOTTOM
        barChart.xAxis.granularity = 1f
        barChart.xAxis.isGranularityEnabled = true
        barChart.description.isEnabled = false
        barChart.animateY(1000)
        barChart.invalidate()
    }
}

How does the bar chart label its X-axis?

The IndexAxisValueFormatter maps numeric indices (0, 1, 2...) to the mood labels you provide. Setting granularity = 1f ensures each bar gets exactly one label with no decimals between them.

  • Save AnalyticsFragment.kt.
  • Run the app and open the analytics screen.

You should now see both charts. The pie chart shows percentage slices and the bar chart below it shows the same data as absolute spending amounts per mood on the Y-axis.

Bar chart labels overlapping?

This happens when the chart view is too narrow for all labels. Check that your barChart in the layout has layout_width="match_parent" and layout_height="250dp". Also confirm isGranularityEnabled = true is set so labels do not double up.

help me fix overlapping labels on my MPAndroidChart bar chart

Add a month selector and verify the full dashboard

The month navigation buttons are already wired in onViewCreated. Time to test that navigating months re-queries the database and updates both charts.

  • Log at least 2 more test expenses with different moods if you only have data in one month. Use different dates so you have expenses in at least 2 separate months.
  • Open the analytics screen.

Before you tap the month arrows, predict what you expect to see. Will both charts change when you navigate to a month with different expenses?

  • Tap the < button to move to the previous month.

The month label updates and both charts re-render with that month's data. If you navigate to a month with no expenses, the charts appear empty.

  • Tap the > button to return to the current month and confirm your original data reappears.

What should I see?

The pie chart shows proportional mood slices with percentages (for example, "Stressed 60%", "Happy 40%"). The bar chart shows the corresponding absolute amounts on the Y-axis. Both charts use matching colours per mood. Changing the month updates both charts together.

Charts not updating when you change months?

Check that your button click listeners call both updateMonthLabel() and loadData() in sequence. If only the label changes, loadData is not being called.

Also verify that your date calculations use calendar.clone() so the start and end computations do not mutate the main calendar instance.

help me debug month navigation in my analytics dashboard

✔️ Awesome, I've got everything!

Great. Double check you have saved both fragment_analytics.xml and AnalyticsFragment.kt.

ⓧ I'd like to double check the full code

Your fragment_analytics.xml should contain the ScrollView, month navigation row, PieChart, and BarChart blocks you assembled above. Scroll up and confirm all three XML chunks are present in order.

Your AnalyticsFragment.kt should contain, in order:

  • Package and imports block.
  • Class declaration with properties and moodColors map.
  • onCreateView inflating the layout.
  • onViewCreated binding views and wiring button listeners.
  • updateMonthLabel setting the month text.
  • loadData querying expenses and calling both setup methods.
  • setupPieChart building PieEntry list and configuring the chart.
  • setupBarChart building BarEntry list and configuring the chart.
  • Closing brace for the class.

Your analytics dashboard is live. You can now see at a glance where your spending concentrates emotionally and track that pattern month by month. Next up, you will tackle the Secret Mission and turn these patterns into actionable behavioral insights.

Secret mission

Behavioral Insights and Validation Feedback

Your charts show spending patterns, but they do not tell the user what to do about them. In this secret mission, you will generate natural-language behavioral nudges from mood-spending data and implement a validation feedback loop that asks users to confirm detected moods. This strengthens your dissertation's evaluation chapter by measuring algorithm accuracy over time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Mood analytics Kotlin source files (MoodScoringEngine.kt, MoodQuizFragment.kt, AnalyticsFragment.kt, InsightsGenerator.kt, and related files).
  • Room database entities and DAOs (MoodQuestion.kt, MoodValidation.kt, MoodQuestionDao.kt, MoodValidationDao.kt).
  • XML layout files (fragment_mood_quiz.xml, fragment_analytics.xml, fragment_expense_history.xml, item_expense.xml).
  • MPAndroidChart library dependency (v3.1.0) added to your Gradle files.
  • Local Room database with seeded mood questions and stored validations.

Keep everything running

No action needed. Choose this if you are still actively building your dissertation project or plan to continue extending the mood analytics feature.

  • Your project remains in Android Studio exactly as you left it.
  • The Room database with your 50 seeded questions and any logged expenses persists on the emulator or device.
  • MPAndroidChart stays in your Gradle dependencies, ready for further chart customisation.

Pause - I'll come back to this later

Free up system resources by stopping the emulator and closing Android Studio. Your project files stay on disk.

  • Stop any running emulator by clicking the square stop button in Android Studio's toolbar.
  • Close Android Studio. Your project files remain on disk and will reopen when you launch Android Studio again.
  • When you return, open Android Studio, select your project, and let Gradle sync complete before running.

Delete - I don't want to use this again

Remove the mood analytics feature files you added during this project. This reverts your expense tracker to its pre-project state.

  • In Android Studio, delete the mood analytics source files from your project. Right-click each file in the Project panel and select Delete. Remove: MoodQuestion.kt, MoodQuestionDao.kt, MoodValidation.kt, MoodValidationDao.kt, MoodQuestionSeeder.kt, MoodResult.kt, MoodScoringEngine.kt, and InsightsGenerator.kt.
  • Delete the UI fragments: MoodQuizFragment.kt, AnalyticsFragment.kt, and ExpenseHistoryFragment.kt.
  • Delete the XML layout files: fragment_mood_quiz.xml, fragment_analytics.xml, fragment_expense_history.xml, and item_expense.xml.
  • In your module-level build.gradle, remove the MPAndroidChart dependency line: implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'.
  • In your settings.gradle, remove the JitPack repository line: maven { url = uri("https://jitpack.io") }.
  • Revert your AppDatabase.kt to remove the MoodQuestion and MoodValidation entities, their DAOs, and the seeder callback. Decrement the database version back to its previous value.
  • Uninstall the app from your emulator or device to clear the local Room database. In the emulator, long-press the app icon and select Uninstall.
  • Sync Gradle to confirm the project builds without the removed dependencies.

That's a Wrap!

That's a Wrap!

Nice work! You have built a full mood-expense analytics engine that detects emotional states through a weighted quiz, tags every expense with that mood, and surfaces monthly spending patterns through interactive charts and behavioral nudges.

You've learned how to:

  • Build a weighted scoring engine that selects 3 questions from a 50-question bank, scores answers across 5 mood categories, and calculates detection confidence as a percentage.
  • Wire a mood quiz into an expense entry flow that stores mood-tagged expenses in Room, then display them in a filterable history with mood chips and date range selection.
  • Create a monthly analytics dashboard using MPAndroidChart with a pie chart showing percentage breakdowns and a bar chart showing absolute spending amounts per mood.
  • Secret Mission: Generated behavioral insight nudges from mood-spending patterns and implemented a validation feedback loop that tracks algorithm accuracy over time.

Ready to quiz yourself?