0% found this document useful (0 votes)
31 views85 pages

Expense Manager with AI Integrated Thesis File

GoodBye Money is an AI-powered expense management application developed using Kotlin, Jetpack Compose, and Realm Database, designed to help users track their finances with features like categorized expense tracking and AI-driven financial insights. The app addresses the limitations of traditional and existing digital expense trackers by providing offline functionality and personalized budget recommendations. It follows the MVVM architecture for maintainability and aims to enhance financial decision-making through interactive reports and data visualization.

Uploaded by

akbar
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
31 views85 pages

Expense Manager with AI Integrated Thesis File

GoodBye Money is an AI-powered expense management application developed using Kotlin, Jetpack Compose, and Realm Database, designed to help users track their finances with features like categorized expense tracking and AI-driven financial insights. The app addresses the limitations of traditional and existing digital expense trackers by providing offline functionality and personalized budget recommendations. It follows the MVVM architecture for maintainability and aims to enhance financial decision-making through interactive reports and data visualization.

Uploaded by

akbar
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 85

2025

Goodbye
Money
An AI-Powered Expense Manager Using Jetpack
Compose & Realm Database

GoodBye Money is a smart and intuitive expense management application designed to


help users track their finances effortlessly. Built using Kotlin and Jetpack Compose, it
provides powerful features such as categorized expense tracking, insightful financial
reports, AI-powered assistance, and a seamless user experience. With a focus on
simplicity, security, and efficiency, this app empowers users to take control of their
financial well-being, ensuring a better and more organized future.
Abstract 5
Abstract 5
Keywords: Expense Manager, AI-Powered Finance, Kotlin, Jetpack Compose, Realm
Database, MVVM Architecture. 6
Acknowledgment 7
Chapter 1: Introduction 8
1.1 Background 8
1.2 Problem Statement 8
1.3 Objectives 9
1.4 Significance of the Study 9
1.5 Scope of the Project 10
Chapter 2: Literature Review 11
2.1 Introduction 11
2.2 Traditional Expense Tracking Methods 11
2.3 Digital Expense Tracking Applications 12
Limitations of Existing Apps 12
2.4 The Role of Artificial Intelligence in Finance 13
2.4 The Role of Artificial Intelligence in Finance 13
2.5 The Importance of Offline-First Architecture in Mobile Apps 13
2.5.1 Need for Offline Functionality 13
2.5.2 Realm Database vs. SQLite vs. Room 13
2.6 Jetpack Compose for Modern UI Development 14
Limitations of XML-Based UI 14
2.7 MVVM Architecture for Scalability and Maintainability 14
Comparison with Other Architectures 14
2.8 Summary of Literature Review 15
Chapter 3: System Analysis and Design 16
3.1 Introduction 16
3.2 Functional Requirements 16
3.3 Non-Functional Requirements 17
3.4 System Architecture 18
3.5 Use Case Diagram 19
3.6 Database Design 20
3.7 UI/UX Design 21
3.8 Summary of System Analysis and Design 27
Chapter 4: Implementation and Development 28
4.1 Project Setup and Architecture 29
4.1.1 Tech Stack Selection 29
4.1.2 Project Structure (Based on Image) 30
4.2 Database Implementation (db.kt) 30
4.2.1 Why Realm? 30
4.2.2 Database Schema Design (db.kt) 30
4.2.3 Category Model (models/Category.kt) 30
4.2.4 Expense Model (models/Expense.kt) 31
4.2.3 Recurrence Model (models/Recurrence.kt) 33
4.3 Expense Tracking and UI Components (components/) 34
4.3.1 Expense Row (ExpenseRow.kt) 34
4.3.2 Expense Grouping (ExpensesDayGroup.kt) 35
4.3.3 CategoryBadge(CategoryBadge.kt) 36
4.3.4 PickerTrigger(PickerTrigger.kt) 36
4.3.5 ReportPage(ReportPage.kt) 37
4.3.6 TableRow(TableRow.kt) 39
4.3.7 UnstyledTextField(UnstyledTextField.kt) 40
4.3.8 ExpensesList(expenseList/ExpensesList.kt) 41
4.4 Report Generation (components/charts/) 42
4.4.1 BarDrawer (charts/BarDrawer.kt) 42
4.4.2 LabelDrawer (charts/LabelDrawer.kt) 43
4.4.3 MonthlyChart(charts/MonthlyChart.kt) 44
4.4.4 WeeklyChart (charts/WeeklyChart.kt) 45
4.4.5 YearlyChart (charts/YearlyChart.kt) 46
4.5 API Integration (network/) 48
4.5.1 Setting Up Retrofit 48
4.5.2 API Client 48
4.5.3 API Call from ViewModel 49
4.6 ViewModel Integration (viewmodels/) 49
4.6.1 AddViewModel 49
4.6.2 AssitantViewModel 51
4.6.3 CategoriesViewModel 52
4.6.4 ExpensesViewModel 54
4.6.5 Factory 55
4.6.6 ReportPageViewModel 55
4.6.7 ReportViewModel 56
4.7 Screens Implementation (pages/) 57
4.7.1 Add Screen (pages/add.kt) 57
4.7.2 Assistant Screen (pages/AssistantScreen.kt) 60
4.7.3 Categories Screen (pages/Categories.kt) 62
4.7.4 Expenses Screen (pages/Expenses.kt) 65
4.7.5 Reports Screen (pages/Reports.kt) 67
4.7.6 Settings Screen (pages/Settings.kt) 68
4.8 Main Activity (MainActivity.kt) 70
4.9 Gradle (Module:app) 73
4.10 Summary of Implementation and Development 74
Chapter 5: Deployment and Maintenance 75
5.1 Preparing the Application for Deployment 75
5.1.1 Code Optimization and Minification 75
5.1.2 Generating a Signed APK/AAB 75
5.2 Play Store Submission Process 76
5.2.1 Creating the Play Store Listing 76
5.2.2 Google Play Review Process 76
5.3 Post-Release Monitoring and Maintenance 77
5.3.1 Crash and Performance Monitoring 77
5.3.2 Regular Updates and Bug Fixes 77
5.4 Future Enhancements 77
5.4.1 AI-Driven Expense Insights 77
5.4.2 Multi-Platform Expansion 77
5.4.3 Cloud Backup and Sync 77
5.5 Summary 77
Chapter 6: Conclusion and Final Thoughts 79
6.1 Summary of the Project 79
6.1.1 Overview 79
6.1.2 Achievements and Contributions 80
6.2 Challenges Faced and Solutions Implemented 80
6.2.1 UI Performance Optimization 80
6.2.2 Database Performance Issues 81
6.2.3 Play Store Policy Compliance 81
6.3 Lessons Learned 82
6.3.1 Android Development Best Practices 82
6.3.2 Project Management & Soft Skills 82
6.4 Future Scope of the Project 82
6.4.1 AI-Enhanced Budgeting 83
6.4.2 Cross-Platform Expansion 83
6.4.3 Cloud Backup and Multi-Device Sync 83
6.4.4 Subscription-Based Premium Features 83
6.5 Personal Reflections 83
6.6 Final Words 83
Acknowledgments 84
Abstract
Abstract
In today’s fast-paced world, managing personal finances efficiently has become
increasingly important. Traditional methods of expense tracking, such as manual
record-keeping and spreadsheets, are time-consuming and lack real-time financial
insights. Modern expense management applications offer basic tracking features,
but most fail to provide intelligent financial suggestions, personalized insights, and
offline-first functionality.

This project presents GoodBye Money, an AI-powered expense manager built


using Kotlin, Jetpack Compose, and Realm Database. Unlike conventional expense
trackers, GoodBye Money integrates an OpenAI-based financial assistant,
providing users with intelligent spending insights, personalized budget
recommendations, and automated financial reports. The application follows the
MVVM architecture to ensure scalability and maintainability, while Realm
Database enables efficient offline storage and fast queries.

Key features of the application include:


✅ Expense Tracking – Users can record, edit, delete, and categorize expenses
efficiently.
✅ AI Financial Insights – An integrated OpenAI-powered chatbot offers spending
analysis and budget recommendations.
✅ Data Visualization – Interactive charts and graphical reports display expense
trends.
✅ Offline-first Architecture – Realm Database ensures smooth operation without
internet connectivity.
✅ User-Friendly UI – Built using Jetpack Compose for a smooth, intuitive
experience.
✅ Data Security & Management – Users can erase data securely with a single tap.

The system was developed following a systematic software engineering approach,


including requirement analysis, system design, implementation, and rigorous testing.
The application was evaluated based on performance benchmarks, user experience
tests, and AI accuracy tests to ensure reliability and efficiency.

The results demonstrate that GoodBye Money significantly enhances expense


tracking and financial decision-making compared to traditional methods. The
AI-powered insights help users understand their spending patterns better and
make informed financial decisions. Additionally, Realm Database proves to be a
highly efficient storage solution, outperforming SQLite in terms of speed and offline
usability.

This project contributes to the field of financial technology (FinTech) by combining


AI with modern mobile application development. Future enhancements may
include cloud synchronization, smart notifications, and predictive expense
analysis to further improve financial management for users.
Keywords: Expense Manager, AI-Powered Finance, Kotlin, Jetpack
Compose, Realm Database, MVVM Architecture.
Acknowledgment
I would like to express my deepest gratitude to Almighty Allah for
giving me the strength, knowledge, and perseverance to complete this
project successfully. This journey has been one of the most rewarding
and challenging experiences of my academic career.

I extend my heartfelt appreciation to my supervisor, for their


invaluable guidance, constructive feedback, and continuous
encouragement throughout the development of this project. Their
expertise and insights have played a crucial role in shaping this
research and improving my technical and analytical skills.

I am incredibly grateful to my family, especially my parents, for their


unwavering support, patience, and belief in me. Their sacrifices,
prayers, and encouragement have been the driving force behind my
success.

My sincere thanks go to my teachers at [University Name], who have


provided me with the knowledge and skills necessary to undertake this
project. Their dedication to teaching and mentoring has been a source
of inspiration.

I also want to acknowledge the contributions of my friends and group


members, [Names of Group Members], who have provided constant
motivation, discussions, and technical support whenever needed. Their
collaboration and ideas have greatly enriched this project.

Finally, I would like to express my appreciation to the open-source


community and developers whose work and documentation helped
me understand various concepts and frameworks used in this project.
Chapter 1: Introduction
(This chapter introduces the project, explains the background, problem statement,
objectives, and significance.)

1.1 Background
Managing personal finances is an essential part of everyday life, yet many individuals
struggle to track their expenses effectively. Studies show that over 65% of people
do not maintain a budget, leading to poor financial decisions, unnecessary spending,
and financial stress. While digital tools have simplified financial management, most
existing expense tracking applications lack AI-driven insights and seamless
offline performance.

Traditional expense tracking methods include:

 Manual bookkeeping – Writing down expenses in notebooks (prone to


human errors).
 Spreadsheets – Excel sheets require manual data entry and lack intelligent
insights.
 Expense tracking apps – Many apps are available but suffer from limitations
such as:
o Limited offline functionality (most use cloud storage).
o No AI-powered insights for personalized financial suggestions.
o Complex and outdated UI leading to poor user experience.

This project aims to bridge these gaps by developing GoodBye Money, a modern
AI-powered expense tracker that combines intelligent financial insights with fast,
efficient offline storage.

1.2 Problem Statement


Existing expense trackers provide basic logging functionalities but fail to offer
smart financial recommendations. Additionally, most applications rely on
cloud-based storage, requiring internet access for full functionality. This dependency
leads to data synchronization issues, security concerns, and performance
bottlenecks.

This research aims to develop a smart, offline-first expense manager with


AI-driven insights to help users manage their finances more effectively.
1.3 Objectives
The primary objectives of this project are:

1. Develop a smart expense tracking system that helps users efficiently


manage their finances.
2. Integrate OpenAI-powered financial insights to provide spending
recommendations and budget predictions.
3. Implement an offline-first approach using Realm Database for fast, local
storage.
4. Ensure a seamless, modern UI using Jetpack Compose.
5. Follow MVVM architecture for structured and maintainable code.
6. Provide interactive charts and financial reports for better spending
analysis.
7. Allow users to securely erase data when needed.

Image: Flowchart of app functionality.

1.4 Significance of the Study


This project is significant because it:

 Bridges the gap between traditional expense tracking and AI-powered


automation.
 Eliminates the need for internet connectivity by utilizing Realm Database
for local storage.
 Provides intelligent financial insights to help users save money and
improve budgeting.
 Contributes to financial technology (FinTech) by integrating AI in
personal finance management.
 Offers a scalable and modern solution using Jetpack Compose and
MVVM architecture.

1.5 Scope of the Project


The GoodBye Money app focuses on:

 Tracking and categorizing expenses.


 Providing AI-based spending insights.
 Generating visual reports for spending analysis.
 Supporting offline-first data storage.

However, the following features are not included in the current version but may be
added in future releases:
❌ Cloud synchronization across multiple devices.
❌ Multi-user support.
❌ Predictive expense forecasting based on past spending trends.
Chapter 2: Literature Review
(This chapter reviews existing research, technologies, and frameworks related to
expense management, AI integration, and mobile app development. It provides a
foundation for understanding the need for an AI-powered, offline-first expense
manager.)

2.1 Introduction
Expense management is an essential aspect of personal finance. Many individuals
struggle to track their expenses, leading to poor budgeting and financial instability.
Over the years, various technologies and methodologies have been developed to
automate and simplify expense tracking.

This chapter reviews existing research on:

 Traditional vs. digital expense tracking.


 The role of Artificial Intelligence (AI) in financial management.
 The importance of offline-first databases for mobile applications.
 Modern UI frameworks like Jetpack Compose.
 The MVVM architecture for scalable app development.

The findings highlight current gaps in existing solutions and justify the need for
GoodBye Money, an AI-powered expense manager with offline-first capabilities.

Attach Image: Diagram showing evolution of expense tracking methods


(manual > spreadsheets > mobile apps > AI-driven finance).

2.2 Traditional Expense Tracking Methods


Historically, people have used manual methods to track expenses, such as:

1. Physical Notebooks – Writing down transactions manually.


2. Spreadsheets – Using Excel or Google Sheets for expense calculations.
3. Bank Statements – Reviewing monthly bank statements for financial
tracking.

These methods have significant limitations:

 High risk of human error in manual entry.


 Lack of real-time insights or spending trends.
 Time-consuming and not scalable for large financial data.

Image: A comparison table of traditional vs. digital expense tracking methods.


2.3 Digital Expense Tracking Applications
With the rise of smartphones, digital expense tracking apps have gained popularity.
Some popular apps include:

 Mint – Tracks spending, categorizes transactions, and offers budgeting tools.


 YNAB (You Need a Budget) – Focuses on proactive budgeting techniques.
 PocketGuard – Shows how much disposable income users have left after expenses.

Limitations of Existing Apps

Despite their usefulness, these applications have several drawbacks:


❌ Require internet access – Most rely on cloud storage, limiting offline usability.
❌ Lack AI-powered insights – They only display spending data without offering
intelligent recommendations.
❌ Complex user interfaces – Some have overwhelming dashboards, making them
hard for beginners.
❌ Security concerns – Many apps require bank account linking, increasing risk
exposure.

Attach Image: A graph comparing user satisfaction of various finance apps based
on offline accessibility, AI insights, and ease of use.
2.4 The Role of Artificial Intelligence in Finance
2.4 The Role of Artificial Intelligence in Finance

Artificial Intelligence (AI) is transforming financial management by offering smart


automation, predictive analysis, and personalized financial insights. AI-powered
applications analyze spending patterns, predict future expenses, and suggest
budgeting strategies.

2.4.1 AI-Based Financial Assistants

AI chatbots like OpenAI's GPT and Google Bard help users:


✅ Understand spending trends.
✅ Get personalized budget recommendations.
✅ Receive real-time insights on savings and expenditures.

2.4.2 Predictive Expense Analysis

AI can use machine learning (ML) algorithms to:

 Forecast future expenses based on historical data.


 Identify spending anomalies (e.g., sudden high expenditures).
 Provide dynamic budgeting advice (adjusting budgets based on spending habits).

Attach Image: AI vs. Traditional Expense Tracking – A table comparing features.

Limitation: While AI offers intelligent insights, most expense tracking apps lack
real-time AI-based analysis, creating an opportunity for innovation.

2.5 The Importance of Offline-First Architecture in


Mobile Apps
2.5.1 Need for Offline Functionality

Most finance apps require constant internet access, which creates problems for users
in areas with poor connectivity.

2.5.2 Realm Database vs. SQLite vs. Room

Feature Realm Database SQLite Room (Jetpack)


Performance Very fast Moderate Good
Ease of Use ✅ Simple API ❌ Complex SQL queries ✅ Simplified ORM
Offline Support ✅ Full offline ✅ Full offline ✅ Full offline
Data Encryption ✅ Yes ❌ No ❌ No
Automatic Syncing ✅ Yes (optional) ❌ No ❌ No
Attach Image: A bar chart comparing Realm, SQLite, and Room in terms of
speed and efficiency.

Conclusion: Realm Database provides faster read/write operations, making it ideal


for an offline-first finance app.

2.6 Jetpack Compose for Modern UI Development


Jetpack Compose is Google’s modern UI toolkit for Android, offering:
✅ Declarative UI – Less code, better performance.
✅ Composable Functions – Reusable components.
✅ Real-Time Previews – Speeds up UI development.
✅ Better State Management – Handles UI updates efficiently.

Limitations of XML-Based UI

❌ Requires boilerplate code.


❌ Harder to maintain complex UIs.
❌ Not optimized for modern Android development.

Conclusion: Jetpack Compose simplifies UI development and improves app


performance, making it the best choice for GoodBye Money.

2.7 MVVM Architecture for Scalability and


Maintainability
Model-View-ViewModel (MVVM) is the preferred architecture for Android apps
due to:
✅ Separation of concerns (UI, logic, and data layers are independent).
✅ Better testability (ViewModels can be tested separately).
✅ Easier maintenance (reduces code complexity).

Comparison with Other Architectures

Architecture Advantages Disadvantages


MVC Simple, widely used Poor separation of concerns
MVP Better testability Still has tight coupling
MVVM Best separation of concerns, scalable Slightly complex for beginners

Conclusion: MVVM ensures scalability and maintainability, making it the best fit
for GoodBye Money.
2.8 Summary of Literature Review
This chapter reviewed:

 Traditional vs. AI-powered expense tracking.


 The importance of offline-first architecture and Realm Database.
 Jetpack Compose for UI development.
 MVVM for structured, scalable code.

Conclusion: Existing solutions lack AI-powered insights, offline-first


architecture, and modern UI frameworks. GoodBye Money bridges this gap by
combining AI, offline storage, and a scalable architecture.

Next Chapter: System Analysis and Design – Understanding requirements, use


cases, and the system’s technical design.
Chapter 3: System Analysis and Design
(This chapter explains how the system was designed, including functional and
non-functional requirements, use case diagrams, system architecture, and database
design.)

3.1 Introduction
This chapter details the analysis and design of GoodBye Money, covering:

 Functional and non-functional requirements.


 System architecture and workflow.
 Database and UI design.

Image: A high-level architecture diagram of GoodBye Money.

3.2 Functional Requirements


Definition: Functional requirements describe the specific behaviors and
functionalities of the application.

ID Requirement Description
FR1 User Registration Users can sign up and manage profiles.
FR2 Expense Entry Users can add, edit, and delete expenses.
FR3 AI Insights AI provides financial insights and recommendations.
FR4 Report Generation Users can generate spending reports with charts.
FR5 Offline Mode App works without internet using Realm Database.
3.3 Non-Functional Requirements
Definition: Non-functional requirements define system performance, security, and
scalability.

 Performance: App should load under 2 seconds.


 Security: User data must be encrypted.
 Scalability: The app should handle large data volumes efficiently.
 Usability: The UI must be intuitive and easy to navigate.

Image: A performance benchmark graph comparing Realm vs. SQLite.


3.4 System Architecture
GoodBye Money follows MVVM architecture with:

 View: Jetpack Compose UI


 ViewModel: Manages UI state
 Repository: Handles data access
 Data Layer: Realm Database + Retrofit for AI API

Image: MVVM architecture diagram with data flow.


3.5 Use Case Diagram
Definition: A use case diagram visually represents system interactions.

Actors:

 User (primary actor) – Adds expenses, views reports, interacts with AI.
 AI System – Processes spending data and provides insights.
 Realm Database – Stores financial records.

Image: UML Use Case Diagram of GoodBye Money.


3.6 Database Design
GoodBye Money uses Realm Database, consisting of:

1. User Table – Stores user profile information.


2. Expense Table – Stores expenses with timestamps.
3. Category Table – Custom categories for expenses.
4. AI Insights Table – Stores AI-generated financial suggestions.

Image: A relational model of GoodBye Money’s database structure.


3.7 UI/UX Design
GoodBye Money follows Material Design 3 principles:
✅ Minimalistic UI – Simple yet powerful interface.
✅ Dark Mode – Supports theme switching.
✅ Intuitive Navigation – Bottom navigation for quick access.

Attach Image: Screenshots of Expenses, Reports, Add, Setting, Category, and


AI Insights screens

.
3.8 Summary of System Analysis and Design
This chapter outlined:

 Functional & non-functional requirements.


 System architecture (MVVM, Realm, AI Integration).
 Database and UI design.
Chapter 4: Implementation and
Development
This chapter explains the technical implementation of GoodBye Money with a
structure aligned to the provided project hierarchy. It covers:

 Project setup and architecture


 Database implementation (Realm) – db.kt
 Expense tracking with UI components – components/expensesList/
 Report generation – components/charts/ and components/expensesList/ReportPage.kt
 API integration – network/
 ViewModel integration – viewmodels/
 Security and performance optimizations
4.1 Project Setup and Architecture
4.1.1 Tech Stack Selection

The project uses modern and scalable technologies:

Component Technology Used Justification


UI
Jetpack Compose Modern, declarative UI development
Framework
Architecture MVVM Scalable and maintainable
Fast, offline-first, and supports
Database Realm
reactive updates
Efficient API calls with error
Networking Retrofit
handling
AI Provides AI-powered financial
OpenAI API
Integration insights
Encrypted Realm DB & Secure
Security Protects sensitive financial data
API calls
Testing JUnit, Espresso Unit and UI testing

Image: A layered diagram of the MVVM architecture.


4.1.2 Project Structure (Based on Image)

com.matchmatrix.goodbyemoney/
│── components/
│ ├── charts/ # Graphs for reports
│ ├── expensesList/ # UI components for expenses
│ ├── CategoryBadge.kt
│ ├── ExpenseRow.kt
│ ├── ExpensesDayGroup.kt
│ ├── PickerTrigger.kt
│ ├── ReportPage.kt
│ ├── TableRow.kt
│ ├── UnstyledTextField.kt
│── mock/ # Test/mock data
│── models/ # Data models
│── network/ # API calls (Retrofit)
│── pages/ # Screens
│── ui.theme/ # App theme
│── utils/ # Helper functions
│── viewmodels/ # ViewModels (MVVM)
│── db.kt # Database logic (Realm)
│── MainActivity.kt # App entry point

4.2 Database Implementation (db.kt)


4.2.1 Why Realm?

 Faster than SQLite and Room ✅


 Offline-first with automatic syncing ✅
 Minimal boilerplate code ✅
 Supports reactive data updates ✅

4.2.2 Database Schema Design (db.kt)

4.2.3 Category Model (models/Category.kt)


package com.matechmatrix.goodbyemoney.models

import androidx.compose.ui.graphics.Color
import io.realm.kotlin.types.ObjectId
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey

class Category() : RealmObject {


@PrimaryKey
var _id: ObjectId = ObjectId.create()

private var _colorValue: String = "0,0,0"


var name: String = ""
val color: Color
get() {
val colorComponents = _colorValue.split(",")
val (red, green, blue) = colorComponents
return Color(red.toFloat(), green.toFloat(), blue.toFloat())
}

constructor(
name: String,
color: Color
) : this() {
this.name = name
this._colorValue = "${color.red},${color.green},${color.blue}"
}
}

4.2.4 Expense Model (models/Expense.kt)


package com.matechmatrix.goodbyemoney.models

import io.realm.kotlin.types.ObjectId
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
import java.time.LocalDate
import java.time.LocalDateTime

class Expense(): RealmObject {


@PrimaryKey
var _id: ObjectId = ObjectId.create()
var amount: Double = 0.0

private var _recurrenceName: String = "None"


val recurrence: Recurrence get() { return _recurrenceName.toRecurrence() }

private var _dateValue: String = LocalDateTime.now().toString()


val date: LocalDateTime get() { return LocalDateTime.parse(_dateValue) }

var note: String = ""


var category: Category? = null

constructor(
amount: Double,
recurrence: Recurrence,
date: LocalDateTime,
note: String,
category: Category,
) : this() {
this.amount = amount
this._recurrenceName = recurrence.name
this._dateValue = date.toString()
this.note = note
this.category = category
}
}

data class DayExpenses(


val expenses: MutableList<Expense>,
var total: Double,
)

fun List<Expense>.groupedByDay(): Map<LocalDate, DayExpenses> {


val dataMap: MutableMap<LocalDate, DayExpenses> = mutableMapOf()

this.forEach { expense ->


val date = expense.date.toLocalDate()

if (dataMap[date] == null) {
dataMap[date] = DayExpenses(
expenses = mutableListOf(),
total = 0.0
)
}

dataMap[date]!!.expenses.add(expense)
dataMap[date]!!.total = dataMap[date]!!.total.plus(expense.amount)
}

dataMap.values.forEach { dayExpenses ->


dayExpenses.expenses.sortBy { expense -> expense.date }
}

return dataMap.toSortedMap(compareByDescending { it })
}

fun List<Expense>.groupedByDayOfWeek(): Map<String, DayExpenses> {


val dataMap: MutableMap<String, DayExpenses> = mutableMapOf()

this.forEach { expense ->


val dayOfWeek = expense.date.toLocalDate().dayOfWeek

if (dataMap[dayOfWeek.name] == null) {
dataMap[dayOfWeek.name] = DayExpenses(
expenses = mutableListOf(),
total = 0.0
)
}

dataMap[dayOfWeek.name]!!.expenses.add(expense)
dataMap[dayOfWeek.name]!!.total =
dataMap[dayOfWeek.name]!!.total.plus(expense.amount)
}

return dataMap.toSortedMap(compareByDescending { it })
}

fun List<Expense>.groupedByDayOfMonth(): Map<Int, DayExpenses> {


val dataMap: MutableMap<Int, DayExpenses> = mutableMapOf()

this.forEach { expense ->


val dayOfMonth = expense.date.toLocalDate().dayOfMonth

if (dataMap[dayOfMonth] == null) {
dataMap[dayOfMonth] = DayExpenses(
expenses = mutableListOf(),
total = 0.0
)
}

dataMap[dayOfMonth]!!.expenses.add(expense)
dataMap[dayOfMonth]!!.total =
dataMap[dayOfMonth]!!.total.plus(expense.amount)
}

return dataMap.toSortedMap(compareByDescending { it })
}

fun List<Expense>.groupedByMonth(): Map<String, DayExpenses> {


val dataMap: MutableMap<String, DayExpenses> = mutableMapOf()

this.forEach { expense ->


val month = expense.date.toLocalDate().month

if (dataMap[month.name] == null) {
dataMap[month.name] = DayExpenses(
expenses = mutableListOf(),
total = 0.0
)
}

dataMap[month.name]!!.expenses.add(expense)
dataMap[month.name]!!.total =
dataMap[month.name]!!.total.plus(expense.amount)
}

return dataMap.toSortedMap(compareByDescending { it })
}

4.2.3 Recurrence Model (models/Recurrence.kt)


package com.matechmatrix.goodbyemoney.models

sealed class Recurrence(val name: String, val target: String) {


object None : Recurrence("None", "None")
object Daily : Recurrence("Daily", "Today")
object Weekly : Recurrence("Weekly", "This week")
object Monthly : Recurrence("Monthly", "This month")
object Yearly : Recurrence("Yearly", "This year")
}

fun String.toRecurrence(): Recurrence {


return when(this) {
"None" -> Recurrence.None
"Daily" -> Recurrence.Daily
"Weekly" -> Recurrence.Weekly
"Monthly" -> Recurrence.Monthly
"Yearly" -> Recurrence.Yearly
else -> Recurrence.None
}
}
4.3 Expense Tracking and UI Components (components/)
4.3.1 Expense Row (ExpenseRow.kt)

Each expense item is displayed as a row:


4.3.2 Expense Grouping (ExpensesDayGroup.kt)

To display expenses grouped by day:

Image: Screenshots of ExpenseRow.kt and ExpensesDayGroup.kt in action.


4.3.3 CategoryBadge(CategoryBadge.kt)
package com.matechmatrix.goodbyemoney.components

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.matechmatrix.goodbyemoney.models.Category
import com.matechmatrix.goodbyemoney.ui.theme.Shapes
import com.matechmatrix.goodbyemoney.ui.theme.Typography

@Composable
fun CategoryBadge(category: Category, modifier: Modifier = Modifier) {
Surface(
shape = Shapes.large,
color = category.color.copy(alpha = 0.25f),
modifier = modifier,
) {
Text(
category.name,
color = category.color,
style = Typography.bodySmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}

4.3.4 PickerTrigger(PickerTrigger.kt)
package com.matechmatrix.goodbyemoney.components

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.matechmatrix.goodbyemoney.R
import com.matechmatrix.goodbyemoney.ui.theme.FillTertiary
import com.matechmatrix.goodbyemoney.ui.theme.GoodbyeMoneyTheme
import com.matechmatrix.goodbyemoney.ui.theme.Shapes
import com.matechmatrix.goodbyemoney.ui.theme.Typography

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerTrigger(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
shape = Shapes.medium,
color = FillTertiary,
modifier = modifier,
onClick = onClick,
) {
Row(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, style = Typography.titleSmall)
Icon(
painterResource(R.drawable.ic_unfold_more),
contentDescription = "Open picker",
modifier = Modifier.padding(start = 10.dp)
)
}
}
}

@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)


@Composable
fun Preview() {
GoodbyeMoneyTheme {
PickerTrigger("this week", onClick = {})
}
}

4.3.5 ReportPage(ReportPage.kt)
package com.matechmatrix.goodbyemoney.components

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.matechmatrix.goodbyemoney.components.charts.MonthlyChart
import com.matechmatrix.goodbyemoney.components.charts.WeeklyChart
import com.matechmatrix.goodbyemoney.components.charts.YearlyChart
import com.matechmatrix.goodbyemoney.components.expensesList.ExpensesList
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.ui.theme.LabelSecondary
import com.matechmatrix.goodbyemoney.ui.theme.Typography
import com.matechmatrix.goodbyemoney.utils.formatDay
import com.matechmatrix.goodbyemoney.utils.formatDayForRange
import com.matechmatrix.goodbyemoney.viewmodels.ReportPageViewModel
import com.matechmatrix.goodbyemoney.viewmodels.viewModelFactory
import java.text.DecimalFormat
import java.time.LocalDate

@Composable
fun ReportPage(
innerPadding: PaddingValues,
page: Int,
recurrence: Recurrence,
vm: ReportPageViewModel = viewModel(
key = "$page-${recurrence.name}",
factory = viewModelFactory {
ReportPageViewModel(page, recurrence)
})
) {
val uiState = vm.uiState.collectAsState().value
Column(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Column {
Text(
"${
uiState.dateStart.formatDayForRange()
} - ${uiState.dateEnd.formatDayForRange()}",
style = Typography.titleSmall
)
Row(modifier = Modifier.padding(top = 4.dp)) {
Text(
"Rs",
style = Typography.bodyMedium,
color = LabelSecondary,
modifier = Modifier.padding(end = 4.dp)
)
Text(DecimalFormat("0.#").format(uiState.totalInRange), style =
Typography.headlineMedium)
}
}
Column(horizontalAlignment = Alignment.End) {
Text("Avg/day", style = Typography.titleSmall)
Row(modifier = Modifier.padding(top = 4.dp)) {
Text(
"Rs",
style = Typography.bodyMedium,
color = LabelSecondary,
modifier = Modifier.padding(end = 4.dp)
)
Text(DecimalFormat("0.#").format(uiState.avgPerDay), style =
Typography.headlineMedium)
}
}
}

Box(
modifier = Modifier
.height(180.dp)
.padding(vertical = 16.dp)
) {
when (recurrence) {
Recurrence.Weekly -> WeeklyChart(expenses = uiState.expenses)
Recurrence.Monthly -> MonthlyChart(
expenses = uiState.expenses,
LocalDate.now()
)
Recurrence.Yearly -> YearlyChart(expenses = uiState.expenses)
else -> Unit
}
}

ExpensesList(
expenses = uiState.expenses, modifier = Modifier
.weight(1f)
.verticalScroll(
rememberScrollState()
)
)
}
}

4.3.6 TableRow(TableRow.kt)
package com.matechmatrix.goodbyemoney.components

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.matechmatrix.goodbyemoney.R
import com.matechmatrix.goodbyemoney.ui.theme.Destructive
import com.matechmatrix.goodbyemoney.ui.theme.TextPrimary
import com.matechmatrix.goodbyemoney.ui.theme.Typography

@Composable
fun TableRow(
modifier: Modifier = Modifier,
label: String? = null,
hasArrow: Boolean = false,
isDestructive: Boolean = false,
detailContent: (@Composable RowScope.() -> Unit)? = null,
content: (@Composable RowScope.() -> Unit)? = null
) {
val textColor = if (isDestructive) Destructive else TextPrimary

Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (label != null) {
Text(
text = label,
style = Typography.bodyMedium,
color = textColor,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
if (content != null) {
content()
}
if (hasArrow) {
Icon(
painterResource(id = R.drawable.chevron_right),
contentDescription = "Right arrow",
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
if (detailContent != null) {
detailContent()
}
}
}
4.3.7 UnstyledTextField(UnstyledTextField.kt)
package com.matechmatrix.goodbyemoney.components

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.matechmatrix.goodbyemoney.ui.theme.Primary
import com.matechmatrix.goodbyemoney.ui.theme.TextPrimary

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UnstyledTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
arrangement: Arrangement.Horizontal = Arrangement.Start,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember
{ MutableInteractionSource() },
shape: Shape = TextFieldDefaults.filledShape,
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
) {
// If color is not provided via the text style, use content color as a default
val textColor = TextPrimary
val mergedTextStyle =
textStyle.merge(TextStyle(color = textColor))

BasicTextField(value = value,
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(Primary),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = value,
visualTransformation = visualTransformation,
innerTextField = innerTextField,
placeholder = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = arrangement,
verticalAlignment = CenterVertically
) {
placeholder?.invoke()
}
},
label = label,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
supportingText = supportingText,
shape = shape,
singleLine = singleLine,
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
contentPadding = PaddingValues(horizontal = 16.dp),
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent,
textColor = TextPrimary,
cursorColor = Primary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
),
)
})
}

4.3.8 ExpensesList(expenseList/ExpensesList.kt)
package com.matechmatrix.goodbyemoney.components.expensesList

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.matechmatrix.goodbyemoney.components.ExpensesDayGroup
import com.matechmatrix.goodbyemoney.mock.mockExpenses
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.models.groupedByDay
import com.matechmatrix.goodbyemoney.ui.theme.GoodbyeMoneyTheme

@Composable
fun ExpensesList(expenses: List<Expense>, modifier: Modifier = Modifier) {
val groupedExpenses = expenses.groupedByDay()

Column(modifier = modifier) {
if (groupedExpenses.isEmpty()) {
Text("No data for selected date range.", modifier = Modifier.padding(top
= 32.dp))
} else {
groupedExpenses.keys.forEach { date ->
if (groupedExpenses[date] != null) {
ExpensesDayGroup(
date = date,
dayExpenses = groupedExpenses[date]!!,
modifier = Modifier.padding(top = 24.dp)
)
}
}
}
}
}

@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun Preview() {
GoodbyeMoneyTheme {
ExpensesList(mockExpenses)
}
}

4.4 Report Generation (components/charts/)


4.4.1 BarDrawer (charts/BarDrawer.kt)
package com.matechmatrix.goodbyemoney.components.charts

import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.DrawScope
import com.github.tehras.charts.bar.BarChartData
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.ui.theme.SystemGray04

class BarDrawer constructor(recurrence: Recurrence) :


com.github.tehras.charts.bar.renderer.bar.BarDrawer {
private val barPaint = Paint().apply {
this.isAntiAlias = true
}

private val rightOffset = when(recurrence) {


Recurrence.Weekly -> 24f
Recurrence.Monthly -> 6f
Recurrence.Yearly -> 18f
else -> 0f
}

override fun drawBar(


drawScope: DrawScope,
canvas: Canvas,
barArea: Rect,
bar: BarChartData.Bar
) {
canvas.drawRoundRect(
barArea.left,
0f,
barArea.right + rightOffset,
barArea.bottom,
16f,
16f,
barPaint.apply {
color = SystemGray04
},
)
canvas.drawRoundRect(
barArea.left,
barArea.top,
barArea.right + rightOffset,
barArea.bottom,
16f,
16f,
barPaint.apply {
color = bar.color
},
)
}
}

4.4.2 LabelDrawer (charts/LabelDrawer.kt)


package com.matechmatrix.goodbyemoney.components.charts

import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.nativeCanvas
import com.github.tehras.charts.piechart.utils.toLegacyInt
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.ui.theme.LabelSecondary

class LabelDrawer(val recurrence: Recurrence, private val lastDay: Int? = -1) :


com.github.tehras.charts.bar.renderer.label.LabelDrawer {
private val leftOffset = when (recurrence) {
Recurrence.Weekly -> 50f
Recurrence.Monthly -> 13f
Recurrence.Yearly -> 32f
else -> 0f
}

private val paint = android.graphics.Paint().apply {


this.textAlign = android.graphics.Paint.Align.CENTER
this.color = LabelSecondary.toLegacyInt()
this.textSize = 42f
}

override fun drawLabel(


drawScope: DrawScope,
canvas: Canvas,
label: String,
barArea: Rect,
xAxisArea: Rect
) {
val monthlyCondition =
recurrence == Recurrence.Monthly && (
Integer.parseInt(label) % 5 == 0 ||
Integer.parseInt(label) == 1 ||
Integer.parseInt(label) == lastDay
)

if (monthlyCondition || recurrence != Recurrence.Monthly)


canvas.nativeCanvas.drawText(
label,
barArea.left + leftOffset,
barArea.bottom + 65f,
paint
)
}
}

4.4.3 MonthlyChart(charts/MonthlyChart.kt)
package com.matechmatrix.goodbyemoney.components.charts

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.tehras.charts.bar.BarChart
import com.github.tehras.charts.bar.BarChartData
import com.github.tehras.charts.bar.renderer.xaxis.SimpleXAxisDrawer
import com.github.tehras.charts.bar.renderer.yaxis.SimpleYAxisDrawer
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.models.groupedByDayOfMonth
import com.matechmatrix.goodbyemoney.ui.theme.LabelSecondary
import com.matechmatrix.goodbyemoney.utils.simplifyNumber
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth

@Composable
fun MonthlyChart(expenses: List<Expense>, month: LocalDate) {
val groupedExpenses = expenses.groupedByDayOfMonth()
val numberOfDays = YearMonth.of(month.year, month.month).lengthOfMonth()

BarChart(
barChartData = BarChartData(
bars = buildList() {
for (i in 1..numberOfDays) {
add(BarChartData.Bar(
label = "$i",
value = groupedExpenses[i]?.total?.toFloat()
?: 0f,
color = Color.White,
))
}
}
),
labelDrawer = LabelDrawer(recurrence = Recurrence.Monthly, lastDay =
numberOfDays),
yAxisDrawer = SimpleYAxisDrawer(
labelTextColor = LabelSecondary,
labelValueFormatter = ::simplifyNumber,
labelRatio = 7,
labelTextSize = 14.sp
),
barDrawer = BarDrawer(recurrence = Recurrence.Monthly),
modifier = Modifier
.padding(bottom = 20.dp)
.fillMaxSize()
)
}
4.4.4 WeeklyChart (charts/WeeklyChart.kt)
package com.matechmatrix.goodbyemoney.components.charts

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.tehras.charts.bar.BarChart
import com.github.tehras.charts.bar.BarChartData
import com.github.tehras.charts.bar.BarChartData.Bar
import com.github.tehras.charts.bar.renderer.bar.SimpleBarDrawer
import com.github.tehras.charts.bar.renderer.label.SimpleValueDrawer
import com.github.tehras.charts.bar.renderer.xaxis.SimpleXAxisDrawer
import com.github.tehras.charts.bar.renderer.yaxis.SimpleYAxisDrawer
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.models.groupedByDayOfWeek
import com.matechmatrix.goodbyemoney.ui.theme.LabelSecondary
import com.matechmatrix.goodbyemoney.utils.simplifyNumber
import java.time.DayOfWeek

@Composable
fun WeeklyChart(expenses: List<Expense>) {
val groupedExpenses = expenses.groupedByDayOfWeek()

BarChart(
barChartData = BarChartData(
bars = listOf(
Bar(
label = DayOfWeek.MONDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.MONDAY.name]?.total?.toFloat()
?: 0f,
color = Color.White,
),
Bar(
label = DayOfWeek.TUESDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.TUESDAY.name]?.total?.toFloat() ?:
0f,
color = Color.White
),
Bar(
label = DayOfWeek.WEDNESDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.WEDNESDAY.name]?.total?.toFloat() ?:
0f,
color = Color.White
),
Bar(
label = DayOfWeek.THURSDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.THURSDAY.name]?.total?.toFloat() ?:
0f,
color = Color.White
),
Bar(
label = DayOfWeek.FRIDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.FRIDAY.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
Bar(
label = DayOfWeek.SATURDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.SATURDAY.name]?.total?.toFloat() ?:
0f,
color = Color.White
),
Bar(
label = DayOfWeek.SUNDAY.name.substring(0, 1),
value = groupedExpenses[DayOfWeek.SUNDAY.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
)
),
labelDrawer = LabelDrawer(recurrence = Recurrence.Weekly),
yAxisDrawer = SimpleYAxisDrawer(
labelTextColor = LabelSecondary,
labelValueFormatter = ::simplifyNumber,
labelRatio = 7,
labelTextSize = 14.sp
),
barDrawer = BarDrawer(recurrence = Recurrence.Weekly),
modifier = Modifier
.padding(bottom = 20.dp)
.fillMaxSize()
)
}

4.4.5 YearlyChart (charts/YearlyChart.kt)


package com.matechmatrix.goodbyemoney.components.charts

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.tehras.charts.bar.BarChart
import com.github.tehras.charts.bar.BarChartData
import com.github.tehras.charts.bar.renderer.yaxis.SimpleYAxisDrawer
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.models.groupedByDayOfWeek
import com.matechmatrix.goodbyemoney.models.groupedByMonth
import com.matechmatrix.goodbyemoney.ui.theme.LabelSecondary
import com.matechmatrix.goodbyemoney.utils.simplifyNumber
import java.time.DayOfWeek
import java.time.Month

@Composable
fun YearlyChart(expenses: List<Expense>) {
val groupedExpenses = expenses.groupedByMonth()

BarChart(
barChartData = BarChartData(
bars = listOf(
BarChartData.Bar(
label = Month.JANUARY.name.substring(0, 1),
value = groupedExpenses[Month.JANUARY.name]?.total?.toFloat()
?: 0f,
color = Color.White,
),
BarChartData.Bar(
label = Month.FEBRUARY.name.substring(0, 1),
value = groupedExpenses[Month.FEBRUARY.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.MARCH.name.substring(0, 1),
value = groupedExpenses[Month.MARCH.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.APRIL.name.substring(0, 1),
value = groupedExpenses[Month.APRIL.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.MAY.name.substring(0, 1),
value = groupedExpenses[Month.MAY.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.JUNE.name.substring(0, 1),
value = groupedExpenses[Month.JUNE.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.JULY.name.substring(0, 1),
value = groupedExpenses[Month.JULY.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.AUGUST.name.substring(0, 1),
value = groupedExpenses[Month.AUGUST.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.SEPTEMBER.name.substring(0, 1),
value = groupedExpenses[Month.SEPTEMBER.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.OCTOBER.name.substring(0, 1),
value = groupedExpenses[Month.OCTOBER.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.NOVEMBER.name.substring(0, 1),
value = groupedExpenses[Month.NOVEMBER.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
BarChartData.Bar(
label = Month.DECEMBER.name.substring(0, 1),
value = groupedExpenses[Month.DECEMBER.name]?.total?.toFloat() ?: 0f,
color = Color.White
),
)
),
labelDrawer = LabelDrawer(recurrence = Recurrence.Yearly),
yAxisDrawer = SimpleYAxisDrawer(
labelTextColor = LabelSecondary,
labelValueFormatter = ::simplifyNumber,
labelRatio = 7,
labelTextSize = 14.sp
),
barDrawer = BarDrawer(recurrence = Recurrence.Yearly),
modifier = Modifier
.padding(bottom = 20.dp)
.fillMaxSize()
)
}
4.5 API Integration (network/)
4.5.1 Setting Up Retrofit
package com.matechmatrix.goodbyemoney.network

import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST

// Define API Request


data class AIRequest(
val model: String = "gpt-3.5-turbo",
val messages: List<Message>
)

data class Message(


val role: String, // "user" or "assistant"
val content: String
)

// Define API Response


data class AIResponse(
val choices: List<Choice>
)

data class Choice(


val message: Message
)

// Retrofit API Interface


interface OpenAIApiService {
@Headers(
"Content-Type: application/json",
"Authorization: Bearer
sk-proj-mDMQvbmqORJOtPLcPBUbQRtWjADl_4C2nT6EEog2g3qWSrYilmrW1xtoarsUMa4hN1QY
-N-UobT3BlbkFJ5dlXXtnYkgmDb7X07FHiOVOK-efekhx3EA5Eop_acc36h1ssyeja9IBbBDvLRe
DehrrkLL_KgA"
)
@POST("v1/chat/completions")
fun getAIResponse(@Body request: AIRequest): Call<AIResponse>
}

4.5.2 API Client


package com.matechmatrix.goodbyemoney.network

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
private const val BASE_URL = "https://api.openai.com/"

val api: OpenAIApiService by lazy {


Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(OpenAIApiService::class.java)
}
}
4.5.3 API Call from ViewModel
// Make API call
viewModelScope.launch {
val request = AIRequest(
messages = _messages
)
try {
val response = withContext(Dispatchers.IO) {
RetrofitInstance.api.getAIResponse(request).execute()
}

if (response.isSuccessful) {
val body = response.body()
if (body != null) {
val reply = body.choices.firstOrNull()?.message?.content
if (reply != null) {
_messages.add(Message(role = "assistant", content = reply))
onResponse(reply)
} else {
onResponse("The response contained no valid reply.")
}
} else {
onResponse("Response body is null.")
}
} else {
onResponse("Request failed: ${response.code()}
${response.message()} \n${response.errorBody()?.string()}")
}
} catch (e: Exception) {
onResponse("Error: ${e.message}")
}
}
}

4.6 ViewModel Integration (viewmodels/)


4.6.1 AddViewModel
data class AddScreenState(
val amount: String = "",
val recurrence: Recurrence = Recurrence.None,
val date: LocalDate = LocalDate.now(),
val note: String = "",
val category: Category? = null,
val categories: RealmResults<Category>? = null
)

class AddViewModel : ViewModel() {


private val _uiState = MutableStateFlow(AddScreenState())
val uiState: StateFlow<AddScreenState> = _uiState.asStateFlow()
init {
_uiState.update { currentState ->
currentState.copy(
categories = db.query<Category>().find()
)
}
}

fun setAmount(amount: String) {


var parsed = amount.toDoubleOrNull()

if (amount.isEmpty()) {
parsed = 0.0
}

if (parsed != null) {
_uiState.update { currentState ->
currentState.copy(
amount = amount.trim().ifEmpty { "0" },
)
}
}
}

fun setRecurrence(recurrence: Recurrence) {


_uiState.update { currentState ->
currentState.copy(
recurrence = recurrence,
)
}
}

fun setDate(date: LocalDate) {


_uiState.update { currentState ->
currentState.copy(
date = date,
)
}
}

fun setNote(note: String) {


_uiState.update { currentState ->
currentState.copy(
note = note,
)
}
}

fun setCategory(category: Category) {


_uiState.update { currentState ->
currentState.copy(
category = category,
)
}
}

fun submitExpense() {
if (_uiState.value.category != null) {
viewModelScope.launch(Dispatchers.IO) {
val now = LocalDateTime.now()
db.write {
this.copyToRealm(
Expense(
_uiState.value.amount.toDouble(),
_uiState.value.recurrence,
_uiState.value.date.atTime(now.hour, now.minute,
now.second),
_uiState.value.note,
this.query<Category>("_id == $0",
_uiState.value.category!!._id)
.find().first(),
)
)
}
_uiState.update { currentState ->
currentState.copy(
amount = "",
recurrence = Recurrence.None,
date = LocalDate.now(),
note = "",
category = null,
categories = null
)
}
}
}
}
}

4.6.2 AssitantViewModel
package com.matechmatrix.goodbyemoney.viewmodels

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.matechmatrix.goodbyemoney.network.AIRequest
import com.matechmatrix.goodbyemoney.network.Message
import com.matechmatrix.goodbyemoney.network.RetrofitInstance
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class AssistantViewModel : ViewModel() {


private val _messages = mutableListOf<Message>()
val messages: List<Message> get() = _messages

fun sendMessage(userMessage: String, onResponse: (String) -> Unit)


{
// Add user message to chat
_messages.add(Message(role = "user", content = userMessage))

// Make API call


viewModelScope.launch {
val request = AIRequest(
messages = _messages
)
try {
val response = withContext(Dispatchers.IO) {
RetrofitInstance.api.getAIResponse(request).execute()
}

if (response.isSuccessful) {
val body = response.body()
if (body != null) {
val reply =
body.choices.firstOrNull()?.message?.content
if (reply != null) {
_messages.add(Message(role = "assistant",
content = reply))
onResponse(reply)
} else {
onResponse("The response contained no valid
reply.")
}
} else {
onResponse("Response body is null.")
}
} else {
onResponse("Request failed: ${response.code()}
${response.message()} \n${response.errorBody()?.string()}")
}
} catch (e: Exception) {
onResponse("Error: ${e.message}")
}
}
}
}

4.6.3 CategoriesViewModel
package com.matechmatrix.goodbyemoney.viewmodels

import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.matechmatrix.goodbyemoney.db
import com.matechmatrix.goodbyemoney.models.Category
import io.realm.kotlin.ext.query
import io.realm.kotlin.query.RealmResults
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

data class CategoriesState(


val newCategoryColor: Color = Color.White,
val newCategoryName: String = "",
val colorPickerShowing: Boolean = false,
val categories: List<Category> = listOf()
)

class CategoriesViewModel : ViewModel() {


private val _uiState = MutableStateFlow(CategoriesState())
val uiState: StateFlow<CategoriesState> = _uiState.asStateFlow()

init {
_uiState.update { currentState ->
currentState.copy(
categories = db.query<Category>().find()
)
}
viewModelScope.launch(Dispatchers.IO) {
db.query<Category>().asFlow().collect { changes ->
_uiState.update { currentState ->
currentState.copy(
categories = changes.list
)
}
}
}
}

fun setNewCategoryColor(color: Color) {


_uiState.update { currentState ->
currentState.copy(
newCategoryColor = color
)
}
}

fun setNewCategoryName(name: String) {


_uiState.update { currentState ->
currentState.copy(
newCategoryName = name
)
}
}

fun showColorPicker() {
_uiState.update { currentState ->
currentState.copy(
colorPickerShowing = true
)
}
}

fun hideColorPicker() {
_uiState.update { currentState ->
currentState.copy(
colorPickerShowing = false
)
}
}

fun createNewCategory() {
viewModelScope.launch(Dispatchers.IO) {
db.write {
this.copyToRealm(Category(
_uiState.value.newCategoryName,
_uiState.value.newCategoryColor
))
}
_uiState.update { currentState ->
currentState.copy(
newCategoryColor = Color.White,
newCategoryName = ""
)
}
}
}

fun deleteCategory(category: Category) {


viewModelScope.launch(Dispatchers.IO) {
db.write {
val deletingCategory = this.query<Category>("_id == $0",
category._id).find().first()
delete(deletingCategory)
}
}
}
}

4.6.4 ExpensesViewModel
package com.matechmatrix.goodbyemoney.viewmodels

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.matechmatrix.goodbyemoney.db
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.utils.calculateDateRange
import io.realm.kotlin.ext.query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class ExpensesState(


val recurrence: Recurrence = Recurrence.Daily,
val sumTotal: Double = 1250.98,
val expenses: List<Expense> = listOf()
)

class ExpensesViewModel: ViewModel() {


private val _uiState = MutableStateFlow(ExpensesState())
val uiState: StateFlow<ExpensesState> = _uiState.asStateFlow()

init {
_uiState.update { currentState ->
currentState.copy(
expenses = db.query<Expense>().find()
)
}
viewModelScope.launch(Dispatchers.IO) {
setRecurrence(Recurrence.Daily)
}
}

fun setRecurrence(recurrence: Recurrence) {


val (start, end) = calculateDateRange(recurrence, 0)

val filteredExpenses = db.query<Expense>().find().filter { expense


->
(expense.date.toLocalDate().isAfter(start) &&
expense.date.toLocalDate()
.isBefore(end)) || expense.date.toLocalDate()
.isEqual(start) || expense.date.toLocalDate().isEqual(end)
}

val sumTotal = filteredExpenses.sumOf { it.amount }

_uiState.update { currentState ->


currentState.copy(
recurrence = recurrence,
expenses = filteredExpenses,
sumTotal = sumTotal
)
}
}
}

4.6.5 Factory
package com.matechmatrix.goodbyemoney.viewmodels

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =


object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>):T = f() as T
}

4.6.6 ReportPageViewModel
package com.matechmatrix.goodbyemoney.viewmodels

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.matechmatrix.goodbyemoney.db
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.utils.calculateDateRange
import io.realm.kotlin.ext.query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.LocalTime

data class State(


val expenses: List<Expense> = listOf(),
val dateStart: LocalDateTime = LocalDateTime.now(),
val dateEnd: LocalDateTime = LocalDateTime.now(),
val avgPerDay: Double = 0.0,
val totalInRange: Double = 0.0
)

class ReportPageViewModel(private val page: Int, val recurrence: Recurrence) :


ViewModel() {
private val _uiState = MutableStateFlow(State())
val uiState: StateFlow<State> = _uiState.asStateFlow()

init {
viewModelScope.launch(Dispatchers.IO) {
val (start, end, daysInRange) = calculateDateRange(recurrence, page)

val filteredExpenses = db.query<Expense>().find().filter { expense ->


(expense.date.toLocalDate().isAfter(start) && expense.date.toLocalDate()
.isBefore(end)) || expense.date.toLocalDate()
.isEqual(start) || expense.date.toLocalDate().isEqual(end)
}
val totalExpensesAmount = filteredExpenses.sumOf { it.amount }
val avgPerDay: Double = totalExpensesAmount / daysInRange

viewModelScope.launch(Dispatchers.Main) {
_uiState.update { currentState ->
currentState.copy(
dateStart = LocalDateTime.of(start, LocalTime.MIN),
dateEnd = LocalDateTime.of(end, LocalTime.MAX),
expenses = filteredExpenses,
avgPerDay = avgPerDay,
totalInRange = totalExpensesAmount
)
}
}
}
}
}

4.6.7 ReportViewModel
package com.matechmatrix.goodbyemoney.viewmodels

import androidx.lifecycle.ViewModel
import com.matechmatrix.goodbyemoney.models.Recurrence
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

data class ReportsState(


val recurrence: Recurrence = Recurrence.Weekly,
val recurrenceMenuOpened: Boolean = false
)

class ReportsViewModel: ViewModel() {


private val _uiState = MutableStateFlow(ReportsState())
val uiState: StateFlow<ReportsState> = _uiState.asStateFlow()

fun setRecurrence(recurrence: Recurrence) {


_uiState.update { currentState ->
currentState.copy(
recurrence = recurrence
)
}
}

fun openRecurrenceMenu() {
_uiState.update { currentState ->
currentState.copy(
recurrenceMenuOpened = true
)
}
}

fun closeRecurrenceMenu() {
_uiState.update { currentState ->
currentState.copy(
recurrenceMenuOpened = false
)
}
}
}
4.7 Screens Implementation (pages/)
4.7.1 Add Screen (pages/add.kt)
package com.matechmatrix.goodbyemoney.pages

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import
com.marosseleng.compose.material3.datetimepickers.date.ui.dialog.DatePickerD
ialog
import com.matechmatrix.goodbyemoney.components.TableRow
import com.matechmatrix.goodbyemoney.components.UnstyledTextField
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.ui.theme.*
import com.matechmatrix.goodbyemoney.viewmodels.AddViewModel

@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun Add(navController: NavController, vm: AddViewModel = viewModel()) {
val state by vm.uiState.collectAsState()

val recurrences = listOf(


Recurrence.None,
Recurrence.Daily,
Recurrence.Weekly,
Recurrence.Monthly,
Recurrence.Yearly
)

Scaffold(topBar = {
MediumTopAppBar(
title = { Text("Add") },
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = TopAppBarBackground
)
)
}, content = { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier
.padding(16.dp)
.clip(Shapes.large)
.background(BackgroundElevated)
.fillMaxWidth()
) {
TableRow(label = "Amount", detailContent = {
UnstyledTextField(
value = state.amount,
onValueChange = vm::setAmount,
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("0") },
arrangement = Arrangement.End,
maxLines = 1,
textStyle = TextStyle(
textAlign = TextAlign.Right,
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
)
)
})
Divider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = DividerColor
)
TableRow(label = "Recurrence", detailContent = {
var recurrenceMenuOpened by remember {
mutableStateOf(false)
}
TextButton(
onClick = { recurrenceMenuOpened = true }, shape = Shapes.large
) {
Text(state.recurrence?.name ?: Recurrence.None.name)
DropdownMenu(expanded = recurrenceMenuOpened,
onDismissRequest = { recurrenceMenuOpened = false }) {
recurrences.forEach { recurrence ->
DropdownMenuItem(text = { Text(recurrence.name) }, onClick = {
vm.setRecurrence(recurrence)
recurrenceMenuOpened = false
})
}
}
}
})
Divider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = DividerColor
)
var datePickerShowing by remember {
mutableStateOf(false)
}
TableRow(label = "Date", detailContent = {
TextButton(onClick = { datePickerShowing = true }) {
Text(state.date.toString())
}
if (datePickerShowing) {
DatePickerDialog(onDismissRequest = { datePickerShowing = false },
onDateChange = { it ->
vm.setDate(it)
datePickerShowing = false
},
initialDate = state.date,
title = { Text("Select date", style = Typography.titleLarge) })
}
})
Divider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = DividerColor
)
TableRow(label = "Note", detailContent = {
UnstyledTextField(
value = state.note,
placeholder = { Text("Leave some notes") },
arrangement = Arrangement.End,
onValueChange = vm::setNote,
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
textAlign = TextAlign.Right,
),
)
})
Divider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = DividerColor
)
TableRow(label = "Category", detailContent = {
var categoriesMenuOpened by remember {
mutableStateOf(false)
}
TextButton(
onClick = { categoriesMenuOpened = true }, shape = Shapes.large
) {
Text(
state.category?.name ?: "Select a category first",
color = state.category?.color ?: Color.White
)
DropdownMenu(expanded = categoriesMenuOpened,
onDismissRequest = { categoriesMenuOpened = false }) {
state.categories?.forEach { category ->
DropdownMenuItem(text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
modifier = Modifier.size(10.dp),
shape = CircleShape,
color = category.color
) {}
Text(
category.name, modifier = Modifier.padding(start = 8.dp)
)
}
}, onClick = {
vm.setCategory(category)
categoriesMenuOpened = false
})
}
}
}
})
}
Button(
onClick = vm::submitExpense,
modifier = Modifier.padding(16.dp),
shape = Shapes.large,
enabled = state.category != null
) {
Text("Submit expense")
}
}
})
}

@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)


@Composable
fun PreviewAdd() {
GoodbyeMoneyTheme {
val navController = rememberNavController()
Add(navController = navController)
}
}

4.7.2 Assistant Screen (pages/AssistantScreen.kt)


package com.matechmatrix.goodbyemoney.pages
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowLeft
import androidx.compose.material.icons.rounded.Send
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.matechmatrix.goodbyemoney.components.UnstyledTextField
import com.matechmatrix.goodbyemoney.ui.theme.*
import com.matechmatrix.goodbyemoney.viewmodels.AssistantViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AssistantScreen(navController: NavController, viewModel: AssistantViewModel
= viewModel()) {
val conversations = remember { mutableStateListOf<String>() }
var userInput by remember { mutableStateOf("") }

Scaffold(
topBar = {
MediumTopAppBar(
title = { Text("AI Assistant") },
colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor =
TopAppBarBackground),
navigationIcon = {
Surface(
onClick = navController::popBackStack,
color = Color.Transparent,
) {
Row(modifier = Modifier.padding(vertical = 10.dp)) {
Icon(Icons.Rounded.KeyboardArrowLeft,
contentDescription = "Back")
Text("Settings")
}
}
}
)
},
content = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
LazyColumn(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
items(conversations) { message ->
Text(
text = message,
modifier = Modifier
.padding(8.dp)
.background(BackgroundElevated, Shapes.medium)
.padding(16.dp)
)
}
}

Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
color = BackgroundElevated,
modifier = Modifier
.height(44.dp)
.weight(1f)
.padding(start = 16.dp),
shape = Shapes.large,
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxHeight()
) {
UnstyledTextField(
value = userInput,
onValueChange = { userInput = it },
placeholder = { Text("Ask something...") },
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
)
}
}
IconButton(onClick = {
if (userInput.isNotBlank()) {
conversations.add("You: $userInput")
viewModel.sendMessage(userInput) { reply ->
conversations.add("Assistant: $reply")
}
userInput = ""
}
}) {
Icon(Icons.Rounded.Send, contentDescription = "Send")
}
}

}
}
)
}

@Preview(showBackground = true)
@Composable
fun AssistantScreenPreview() {
AssistantScreen(navController = rememberNavController())
}
4.7.3 Categories Screen (pages/Categories.kt)
package com.matechmatrix.goodbyemoney.pages

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowLeft
import androidx.compose.material.icons.rounded.Send
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.github.skydoves.colorpicker.compose.*
import com.matechmatrix.goodbyemoney.R
import com.matechmatrix.goodbyemoney.components.TableRow
import com.matechmatrix.goodbyemoney.components.UnstyledTextField
import com.matechmatrix.goodbyemoney.ui.theme.*
import com.matechmatrix.goodbyemoney.viewmodels.CategoriesViewModel
import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox

@OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
ExperimentalAnimationApi::class
)
@Composable
fun Categories(
navController: NavController, vm: CategoriesViewModel = viewModel()
) {
val uiState by vm.uiState.collectAsState()

val colorPickerController = rememberColorPickerController()

Scaffold(topBar = {
MediumTopAppBar(title = { Text("Categories") },
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = TopAppBarBackground
),
navigationIcon = {
Surface(
onClick = navController::popBackStack,
color = Color.Transparent,
) {
Row(modifier = Modifier.padding(vertical = 10.dp)) {
Icon(
Icons.Rounded.KeyboardArrowLeft, contentDescription = "Settings"
)
Text("Settings")
}
}
})
}, content = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
AnimatedVisibility(visible = true) {
LazyColumn(
modifier = Modifier
.padding(16.dp)
.clip(Shapes.large)
.fillMaxWidth()
) {
itemsIndexed(
uiState.categories,
key = { _, category -> category.name }) { index, category ->
SwipeableActionsBox(
endActions = listOf(
SwipeAction(
icon = painterResource(R.drawable.delete),
background = Destructive,
onSwipe = { vm.deleteCategory(category) }
),
),
modifier = Modifier.animateItemPlacement()
) {
TableRow(modifier = Modifier.background(BackgroundElevated)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp)
) {
Surface(
color = category.color,
shape = CircleShape,
border = BorderStroke(
width = 2.dp,
color = Color.White
),
modifier = Modifier.size(16.dp)
) {}
Text(
category.name,
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 10.dp
),
style = Typography.bodyMedium,
)
}
}
}
if (index < uiState.categories.size - 1) {
Row(modifier =
Modifier.background(BackgroundElevated).height(1.dp)) {
Divider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = DividerColor
)
}
}
}
}
}
}
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
if (uiState.colorPickerShowing) {
Dialog(onDismissRequest = vm::hideColorPicker) {
Surface(color = BackgroundElevated, shape = Shapes.large) {
Column(
modifier = Modifier.padding(all = 30.dp)
) {
Text("Select a color", style = Typography.titleLarge)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
AlphaTile(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.clip(RoundedCornerShape(6.dp)),
controller = colorPickerController
)
}
HsvColorPicker(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(10.dp),
controller = colorPickerController,
onColorChanged = { envelope ->
vm.setNewCategoryColor(envelope.color)
},
)
TextButton(
onClick = vm::hideColorPicker,
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
) {
Text("Done")
}
}
}
}
}
Surface(
onClick = vm::showColorPicker,
shape = CircleShape,
color = uiState.newCategoryColor,
border = BorderStroke(
width = 2.dp,
color = Color.White
),
modifier = Modifier.size(width = 24.dp, height = 24.dp)
) {}
Surface(
color = BackgroundElevated,
modifier = Modifier
.height(44.dp)
.weight(1f)
.padding(start = 16.dp),
shape = Shapes.large,
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxHeight()
) {
UnstyledTextField(
value = uiState.newCategoryName,
onValueChange = vm::setNewCategoryName,
placeholder = { Text("Category name") },
modifier = Modifier
.fillMaxWidth(),
maxLines = 1,
)
}
}
IconButton(
onClick = vm::createNewCategory,
modifier = Modifier
.padding(start = 16.dp)
) {
Icon(
Icons.Rounded.Send,
"Create category"
)
}
}
}
})
}

@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun CategoriesPreview() {
GoodbyeMoneyTheme {
Categories(navController = rememberNavController())
}
}

4.7.4 Expenses Screen (pages/Expenses.kt)


package com.matechmatrix.goodbyemoney.pages

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.matechmatrix.goodbyemoney.components.PickerTrigger
import com.matechmatrix.goodbyemoney.components.expensesList.ExpensesList
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.ui.theme.GoodbyeMoneyTheme
import com.matechmatrix.goodbyemoney.ui.theme.LabelSecondary
import com.matechmatrix.goodbyemoney.ui.theme.TopAppBarBackground
import com.matechmatrix.goodbyemoney.ui.theme.Typography
import com.matechmatrix.goodbyemoney.viewmodels.ExpensesViewModel
import java.text.DecimalFormat

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Expenses(
navController: NavController,
vm: ExpensesViewModel = viewModel()
) {
val recurrences = listOf(
Recurrence.Daily,
Recurrence.Weekly,
Recurrence.Monthly,
Recurrence.Yearly
)

val state by vm.uiState.collectAsState()


var recurrenceMenuOpened by remember {
mutableStateOf(false)
}

Scaffold(
topBar = {
MediumTopAppBar(
title = { Text("Expenses") },
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = TopAppBarBackground
)
)
},
content = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"Total for:",
style = Typography.bodyMedium,
)
PickerTrigger(
state.recurrence.target ?: Recurrence.None.target,
onClick = { recurrenceMenuOpened = !recurrenceMenuOpened },
modifier = Modifier.padding(start = 16.dp)
)
DropdownMenu(expanded = recurrenceMenuOpened,
onDismissRequest = { recurrenceMenuOpened = false }) {
recurrences.forEach { recurrence ->
DropdownMenuItem(text = { Text(recurrence.target) }, onClick = {
vm.setRecurrence(recurrence)
recurrenceMenuOpened = false
})
}
}
}
Row(modifier = Modifier.padding(vertical = 32.dp)) {
Text(
"Rs",
style = Typography.bodyMedium,
color = LabelSecondary,
modifier = Modifier.padding(end = 4.dp, top = 4.dp)
)
Text(
DecimalFormat("0.#").format(state.sumTotal),
style = Typography.titleLarge
)
}
ExpensesList(
expenses = state.expenses,
modifier = Modifier
.weight(1f)
.verticalScroll(
rememberScrollState()
)
)
}
}
)
}

@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun ExpensesPreview() {
GoodbyeMoneyTheme {
Expenses(navController = rememberNavController())
}
}

4.7.5 Reports Screen (pages/Reports.kt)


package com.matechmatrix.goodbyemoney.pages

import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.matechmatrix.goodbyemoney.R
import com.matechmatrix.goodbyemoney.components.ReportPage
import com.matechmatrix.goodbyemoney.models.Recurrence
import com.matechmatrix.goodbyemoney.ui.theme.TopAppBarBackground
import com.matechmatrix.goodbyemoney.viewmodels.ReportsViewModel

@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Composable
fun Reports(vm: ReportsViewModel = viewModel()) {
val uiState = vm.uiState.collectAsState().value

val recurrences = listOf(


Recurrence.Weekly,
Recurrence.Monthly,
Recurrence.Yearly
)

Scaffold(
topBar = {
MediumTopAppBar(
title = { Text("Reports") },
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = TopAppBarBackground
),
actions = {
IconButton(onClick = vm::openRecurrenceMenu) {
Icon(
painterResource(id = R.drawable.ic_today),
contentDescription = "Change recurrence"
)
}
DropdownMenu(
expanded = uiState.recurrenceMenuOpened,
onDismissRequest = vm::closeRecurrenceMenu
) {
recurrences.forEach { recurrence ->
DropdownMenuItem(text = { Text(recurrence.name) }, onClick = {
vm.setRecurrence(recurrence)
vm.closeRecurrenceMenu()
})
}
}
}
)
},
content = { innerPadding ->
val numOfPages = when (uiState.recurrence) {
Recurrence.Weekly -> 53
Recurrence.Monthly -> 12
Recurrence.Yearly -> 1
else -> 53
}
HorizontalPager(count = numOfPages, reverseLayout = true) { page ->
ReportPage(innerPadding, page, uiState.recurrence)
}
}
)
}

4.7.6 Settings Screen (pages/Settings.kt)


package com.matechmatrix.goodbyemoney.pages

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import com.matechmatrix.goodbyemoney.components.TableRow
import com.matechmatrix.goodbyemoney.db
import com.matechmatrix.goodbyemoney.models.Category
import com.matechmatrix.goodbyemoney.models.Expense
import com.matechmatrix.goodbyemoney.ui.theme.BackgroundElevated
import com.matechmatrix.goodbyemoney.ui.theme.DividerColor
import com.matechmatrix.goodbyemoney.ui.theme.Shapes
import com.matechmatrix.goodbyemoney.ui.theme.TopAppBarBackground
import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Settings(navController: NavController) {
val coroutineScope = rememberCoroutineScope()
var deleteConfirmationShowing by remember {
mutableStateOf(false)
}

val eraseAllData: () -> Unit = {


coroutineScope.launch {
db.write {
val expenses = this.query<Expense>().find()
val categories = this.query<Category>().find()

delete(expenses)
delete(categories)

deleteConfirmationShowing = false
}
}
}

Scaffold(
topBar = {
MediumTopAppBar(
title = { Text("Settings") },
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = TopAppBarBackground
)
)
},
content = { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Column(
modifier = Modifier
.padding(16.dp)
.clip(Shapes.large)
.background(BackgroundElevated)
.fillMaxWidth()
) {
TableRow(
label = "Categories",
hasArrow = true,
modifier = Modifier.clickable {
navController.navigate("settings/categories")
})
Divider(
modifier = Modifier
.padding(start = 16.dp), thickness = 1.dp, color = DividerColor
)
TableRow(
label = "AI Assistant",
hasArrow = true,
modifier = Modifier.clickable {
navController.navigate("settings/assistant")
})
Divider(
modifier = Modifier
.padding(start = 16.dp), thickness = 1.dp, color = DividerColor
)
TableRow(
label = "Erase all data",
isDestructive = true,
modifier = Modifier.clickable {
deleteConfirmationShowing = true
})

if (deleteConfirmationShowing) {
AlertDialog(
onDismissRequest = { deleteConfirmationShowing = false },
title = { Text("Are you sure?") },
text = { Text("This action cannot be undone.") },
confirmButton = {
TextButton(onClick = eraseAllData) {
Text("Delete everything")
}
},
dismissButton = {
TextButton(onClick = { deleteConfirmationShowing = false }) {
Text("Cancel")
}
}
)
}
}
}
}
)
}

4.8 Main Activity (MainActivity.kt)


package com.matechmatrix.goodbyemoney

import android.content.res.Configuration
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.matechmatrix.goodbyemoney.pages.*
import com.matechmatrix.goodbyemoney.ui.theme.GoodbyeMoneyTheme
import com.matechmatrix.goodbyemoney.ui.theme.TopAppBarBackground
import io.sentry.compose.withSentryObservableEffect

class MainActivity : ComponentActivity() {


@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent {
GoodbyeMoneyTheme {
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navController = rememberNavController().withSentryObservableEffect()
val backStackEntry by navController.currentBackStackEntryAsState()

showBottomBar = when (backStackEntry?.destination?.route) {


"settings/categories" -> false
else -> true
}

Scaffold(
bottomBar = {
if (showBottomBar) {
NavigationBar(containerColor = TopAppBarBackground) {
NavigationBarItem(
selected = backStackEntry?.destination?.route == "expenses",
onClick = { navController.navigate("expenses") },
label = {
Text("Expenses")
},
icon = {
Icon(
painterResource(id = R.drawable.upload),
contentDescription = "Upload"
)
}
)
NavigationBarItem(
selected = backStackEntry?.destination?.route == "reports",
onClick = { navController.navigate("reports") },
label = {
Text("Reports")
},
icon = {
Icon(
painterResource(id = R.drawable.bar_chart),
contentDescription = "Reports"
)
}
)
NavigationBarItem(
selected = backStackEntry?.destination?.route == "add",
onClick = { navController.navigate("add") },
label = {
Text("Add")
},
icon = {
Icon(
painterResource(id = R.drawable.add),
contentDescription = "Add"
)
}
)
NavigationBarItem(
selected =
backStackEntry?.destination?.route?.startsWith("settings")
?: false,
onClick = { navController.navigate("settings") },
label = {
Text("Settings")
},
icon = {
Icon(
painterResource(id = R.drawable.settings_outlined),
contentDescription = "Settings"
)
}
)
}
}
},
content = { innerPadding ->
NavHost(
navController = navController,
startDestination = "expenses"
) {
composable("expenses") {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Expenses(navController)
}
}
composable("reports") {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Reports()
}
}
composable("add") {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Add(navController)
}
}
composable("settings") {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Settings(navController)
}
}
composable("settings/categories") {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Categories(navController)
}
}

composable("settings/assistant") {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
AssistantScreen(navController)
}
}
}
}
)
}
}
}
}

@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)


@Composable
fun DefaultPreview() {
GoodbyeMoneyTheme {
Surface {
Greeting("Android")
}
}
}

4.9 Gradle (Module:app)


buildscript {
repositories {
mavenCentral()
}
}

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "io.sentry.android.gradle" version "3.4.2"
id 'io.realm.kotlin'
}

android {
namespace 'com.matechmatrix.goodbyemoney'
compileSdk 34

defaultConfig {
applicationId "com.matechmatrix.goodbyemoney"
minSdk 28
targetSdk 34
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'io.realm.kotlin:library-base:1.6.0'
implementation "androidx.navigation:navigation-compose:2.5.3"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation
"androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material3:material3:1.0.1'
implementation
'com.marosseleng.android:compose-material3-datetime-pickers:0.6.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
implementation "com.github.skydoves:colorpicker-compose:1.0.0"
implementation "me.saket.swipe:swipe:1.0.0"
implementation "io.github.serpro69:kotlin-faker:1.13.0"
implementation "com.github.tehras:charts:0.2.4-alpha"
implementation "com.google.accompanist:accompanist-pager:0.29.1-alpha"
implementation 'io.sentry:sentry-android:6.13.1'
implementation 'io.sentry:sentry-compose-android:6.13.1'

implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation
"androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation
"androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}

4.10 Summary of Implementation and Development


This chapter covered:

 Project setup with MVVM, Jetpack Compose, and Realm.


 Expense tracking UI with components/expensesList/.
 Report generation with charts/ and ReportPage.kt.
 Network API integration with network/.
 Security and performance optimizations.
Chapter 5: Deployment and
Maintenance
This chapter covers the deployment process of GoodBye Money, post-release
maintenance strategies, and potential improvements for future updates. It discusses:

 Generating and signing the app release build


 Play Store listing and submission process
 CI/CD integration for automated deployment
 Crash and performance monitoring
 User feedback collection and future enhancements

5.1 Preparing the Application for Deployment


Before deploying GoodBye Money, several steps were taken to optimize the app and
ensure it met Play Store guidelines.

5.1.1 Code Optimization and Minification

To reduce the app size and improve performance, the following optimizations were
performed:

 ProGuard & R8: Used to remove unused code and obfuscate classes for security.
 Resource Shrinking: Unused resources were removed to minimize APK size.
 Jetpack Compose Optimization: Used LazyColumn for lists and avoided unnecessary
recompositions.

5.1.2 Generating a Signed APK/AAB

Google Play requires an Android App Bundle (AAB) or APK to be signed with a
secure key. The steps followed were:

1. Generated a keystore file using Android Studio.


2. Signed the APK/AAB with the private key.
3. Enabled Google Play App Signing for better key management.
4. Uploaded the signed bundle to the Play Console.

Attach Image: Screenshot of Android Studio Generate Signed Bundle/APK


window.
5.2 Play Store Submission Process
Once the app was optimized and signed, the next step was submitting it to Google
Play Store.

5.2.1 Creating the Play Store Listing

The following information was provided in the Play Console:

 App Name: GoodBye Money - Expense Tracker


 Short Description: A powerful personal finance tracker with AI insights.
 Full Description: Detailed explanation of features, screenshots, and benefits.
 Screenshots: Added for different screen sizes (phone and tablet).
 Feature Graphic & App Icon: Designed a high-resolution icon (512x512) and a banner
(1024x500).
 Privacy Policy: Hosted on a dedicated website.

5.2.2 Google Play Review Process

Before publishing, Google Play reviewed the app for compliance with policies,
including:

 Data safety & permissions disclosure


 Content and ad policies compliance
 Performance and stability testing

The app was approved in 3 days and made available worldwide!


5.3 Post-Release Monitoring and Maintenance
After deployment, continuous monitoring was essential to ensure the app remained
stable, secure, and user-friendly.

5.3.1 Crash and Performance Monitoring

To track crashes and performance, GoodBye Money was integrated with:

✅ Sentry for crash tracking


✅ Firebase Performance Monitoring
✅ Google Play Developer Console’s ANR & Crash Reports

5.3.2 Regular Updates and Bug Fixes

User feedback and performance monitoring helped identify areas for improvement.
Updates included:

 Bug fixes for reported crashes.


 Performance optimizations based on slow UI response reports.
 Feature enhancements based on user suggestions.

5.4 Future Enhancements


To keep GoodBye Money competitive, several enhancements were planned.

5.4.1 AI-Driven Expense Insights

 Expanding the AI assistant to provide personalized financial tips.


 Adding expense prediction models based on spending history.

5.4.2 Multi-Platform Expansion

 Developing a Flutter version for iOS support.


 Creating a web-based dashboard for financial tracking.

5.4.3 Cloud Backup and Sync

 Allowing users to sync data across multiple devices.


 Secure cloud storage integration for automatic backups.

5.5 Summary
 ✅ Optimized and signed the app for Play Store.
 ✅ Successfully deployed to Google Play.
 ✅ Implemented post-release monitoring with Firebase and Sentry.
 ✅ Planned future updates for AI insights, multi-platform support, and cloud backup.
Chapter 6: Conclusion and Final
Thoughts
This chapter provides a comprehensive summary of the GoodBye Money project,
discussing the key takeaways, challenges faced, lessons learned, and future
possibilities. The journey from conceptualization to development, deployment, and
maintenance has been an enriching experience, contributing significantly to my
Android development expertise, problem-solving skills, and software engineering
knowledge.

6.1 Summary of the Project


6.1.1 Overview

GoodBye Money is a Kotlin and Jetpack Compose-based expense tracking


application designed to help users efficiently manage their finances. The app offers:

✅ Aesthetic and user-friendly UI built with Jetpack Compose.


✅ Powerful data management using the Realm database.
✅ Expense categorization and filtering with a customizable color-coded system.
✅ AI-driven insights via OpenAI integration for smart financial assistance.
✅ Data visualization through charts and reports.
✅ Seamless deployment and monitoring on Google Play.

Image: Application architecture diagram showing the MVVM structure.


6.1.2 Achievements and Contributions

This project contributed to:

Advancing my knowledge in modern Android development (Jetpack Compose,


Kotlin Coroutines, MVVM).
Gaining real-world experience in project structuring and software lifecycle
management.
Learning Play Store deployment strategies (signing, optimizing, and publishing).
Developing debugging and performance monitoring skills using Firebase and
Sentry.

The knowledge gained from this project extends beyond GoodBye Money and will be
instrumental in my future Android development endeavors.

6.2 Challenges Faced and Solutions Implemented


During the development and deployment of GoodBye Money, I encountered various
technical, structural, and operational challenges. Below are some of the key
hurdles and the solutions I implemented:

6.2.1 UI Performance Optimization

Problem:

 Handling large lists of expenses in Jetpack Compose without performance issues.


 Ensuring smooth UI interactions and animations.

Solution:
✅ Implemented LazyColumn to optimize rendering for large lists.
✅ Used remember {} and derivedStateOf {} to minimize unnecessary
recompositions.
✅ Followed best practices for state hoisting to improve UI responsiveness.

Image: Code snippet demonstrating LazyColumn usage.


6.2.2 Database Performance Issues

Problem:

 Realm queries were initially slow when retrieving large datasets.


 Needed efficient ways to filter and sort expenses dynamically.

Solution:
✅ Optimized queries using Realm LiveData with Flow for reactive updates.
✅ Indexed frequently queried fields for faster lookups.
✅ Implemented background threading (using Kotlin Coroutines) to prevent UI lag.

Attach Image: Code snippet showing an optimized Realm query.

6.2.3 Play Store Policy Compliance

Problem:

 The app initially faced privacy concerns due to permissions required for AI insights and
storage access.
 Google Play rejected the first submission due to missing Data Safety disclosures.

Solution:
✅ Created a detailed Privacy Policy hosted on a website.
✅ Reduced unnecessary permissions and requested them only when needed.
✅ Clearly outlined data collection and security practices in the Play Console..

6.3 Lessons Learned


Working on GoodBye Money was a transformative experience, reinforcing key
technical and non-technical skills.

6.3.1 Android Development Best Practices

MVVM Architecture:

 Using ViewModels and Repositories provided better scalability and maintainability.

Jetpack Compose Efficiency:

 Handling recomposition effectively significantly improved UI performance.

Realm Database Optimization:

 Using Flow and LiveData enhanced reactive state management.

Efficient API Calls:

 Using Retrofit with Coroutines improved network call management.

6.3.2 Project Management & Soft Skills

Time Management:

 Working within deadlines while ensuring code quality and functionality.

Problem-Solving Skills:

 Debugging complex UI and database issues sharpened my analytical thinking.

User-Centric Approach:

 Iteratively improving the app based on user feedback and analytics.

6.4 Future Scope of the Project


Although GoodBye Money is a fully functional application, there is significant room
for growth and innovation. Some of the planned enhancements include:
6.4.1 AI-Enhanced Budgeting

 Predictive analytics to suggest personalized budgeting tips.

6.4.2 Cross-Platform Expansion

 Flutter version for iOS and Web Dashboard for a seamless multi-device
experience.

6.4.3 Cloud Backup and Multi-Device Sync

 Integration with Google Drive or Firebase Cloud Firestore to ensure secure


data synchronization across devices.

6.4.4 Subscription-Based Premium Features

 Adding premium features such as advanced analytics, AI-driven financial


predictions, and expense tracking for businesses.
 Attach Image: Concept wireframe of GoodBye Money Web Dashboard.

6.5 Personal Reflections


Developing GoodBye Money has been one of the most rewarding projects of my
career.

“This project strengthened my expertise in modern Android development


frameworks, database optimizations, and deployment strategies. It was an incredible
learning journey that will help shape my future projects and professional growth.”

The combination of technology, user needs, and AI integration makes this app a
powerful and innovative financial tool. I look forward to:

 Implementing the planned enhancements.


 Expanding the user base through marketing strategies.
 Exploring new technologies to further optimize the app.

6.6 Final Words


GoodBye Money is not just an expense tracker—it is a smart, AI-driven personal
finance manager that empowers users to take full control of their spending habits.
The journey of developing this app has been filled with:

Challenges that strengthened my problem-solving skills.


New technologies that expanded my development expertise.
Valuable lessons that will shape my future projects.
With further iterations and improvements, GoodBye Money has the potential to
become a leading financial management tool in the market.

Acknowledgments
I would like to express my sincere gratitude to:

 My family for their unconditional support.


 My professors and mentors for their guidance.
 The Android development community for providing open-source resources.
 My peers and project teammates for their collaboration and feedback.
Thank you

You might also like