Build Investment Tracker App in Jetpack Compose
In this tutorial, you will build a complete Investment Tracker app from scratch using Jetpack Compose. This is not a basic demo. You will go through the real development process including UI design, architecture setup, and data handling.
What You Will Build
This app allows users to:
- Add different types of investments like stocks, mutual funds, savings, and livestock
- Edit or delete investments
- Track total investment value dynamically
- See investment count and detailed list
- Switch between light and dark theme automatically
App Features Overview
- Single screen UI with clean layout
- Dashboard cards showing total investment and count
- LazyColumn for smooth list rendering
- Add Investment dialog
- Clean Architecture structure
- In-memory database (will upgrade later)
Project Setup
Create a new Empty Compose Activity project and update dependencies to latest versions.
android { compileSdk 34 }
dependencies {
implementation(platform("androidx.compose:compose-bom:latest"))
}
Material Theme Setup
Use Material Theme Builder to generate a custom theme and replace the default theme files.
- Download theme
- Replace Color.kt, Theme.kt, Type.kt
- Apply theme in MainActivity
Watch Full Tutorial
Clean Architecture Setup
Create the following layers:
- data
- domain
- presentation
- di
Move UI and MainActivity into the presentation layer.
Home Screen UI
The home screen contains:
- Greeting text
- Two dashboard cards
- Investment list
- Floating action button
Dashboard Card Component
@Composable
private fun DashboardItem(
modifier: Modifier,
title: String,
value: String
) {
Card(
modifier = modifier.then(Modifier.height(100.dp)),
colors = CardDefaults.cardColors().copy(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = value,
style = MaterialTheme.typography.titleLarge,
)
}
}
}
}
Investment List using LazyColumn
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
itemsIndexed(
items = state.investments,
key = { index: Int, item: Investment ->
item.id
}
) { index: Int, item: Investment ->
val balance = calculateBalance(item.id, state.transactions)
InvestmentCard(item, balance)
}
}
@Composable
private fun InvestmentCard(
item: Investment,
balance: Double
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors().copy(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = item.type.type,
style = MaterialTheme.typography.labelSmall
)
Text(
text = item.name,
style = MaterialTheme.typography.titleSmall
)
Spacer(
Modifier.height(10.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Invested",
style = MaterialTheme.typography.bodySmall
)
Text(
text = "${balance}",
style = MaterialTheme.typography.titleMedium
)
}
}
}
}
Add Investment Dialog
A dialog allows users to input:
- Investment name
- Type
- Amount
- Date
- Notes
Domain Layer Models
Investment Model
data class Investment(
val id: Int,
val type: InvestmentType,
val name: String
)
Transaction Model
data class Transaction(
val id: Int,
val type: TransactionType,
val investmentId: Int,
val amount: Double,
val date: String,
val description: String
)
In-Memory Database
Instead of storing total amount directly, we calculate it using transactions.
fun addSampleData(){
val investment = Investment(
id = 1,
type = InvestmentType.Stocks,
name = "Apple"
)
InMemoryDatabase.investments.add(InvestmentEntity(
investment.id,
investment.type.type,
investment.name
))
InMemoryDatabase.investments.add(InvestmentEntity(
2,
InvestmentType.Livestock.type,
"Dairy Farm"
))
InMemoryDatabase.transactions.addAll(
listOf(
TransactionEntity(
id = 1,
type = TransactionType.DEPOSIT.type,
investmentId = 1,
amount = 5000.0,
date = "2026-01-01",
description = "Initial investment"
),
TransactionEntity(
id = 2,
type = TransactionType.DEPOSIT.type,
investmentId = 1,
amount = 1000.0,
date = "2026-02-01",
description = "Monthly investment"
),
TransactionEntity(
id = 3,
type = TransactionType.EXPENSE.type,
investmentId = 1,
amount = 100.0,
date = "2026-03-01",
description = "Broker fee"
),
TransactionEntity(
id = 4,
type = TransactionType.DEPOSIT.type,
investmentId = 2,
amount = 1000.0,
date = "2026-03-01",
description = "Initial Investment"
),
TransactionEntity(
id = 5,
type = TransactionType.EXPENSE.type,
investmentId = 2,
amount = 300.0,
date = "2026-04-01",
description = "Land Tax"
)
)
)
}
object InMemoryDatabase {
val investments = mutableListOf<InvestmentEntity>()
val transactions = mutableListOf<TransactionEntity>()
}
Calculate Investment Balance
fun calculateBalance(investmentId: Int, transactions: List<Transaction>): Double{
return transactions.filter { it.investmentId == investmentId }
.sumOf {
when(it.type){
TransactionType.DEPOSIT -> it.amount
TransactionType.EXPENSE -> (it.amount *-1)
TransactionType.WITHDRAW -> (it.amount *-1)
}
}
}
Repository Layer
The repository converts data layer models into domain models.
class InvestmentRepositoryImp(
private val localDataSource: LocalDataSource
): InvestmentRepository {
override suspend fun addTransaction(transaction: Transaction) {
localDataSource.addTransaction(transaction)
}
override suspend fun addInvestment(investment: Investment) {
localDataSource.addInvestment(investment)
}
override suspend fun getTransactions(): List<Transaction> {
val entities = localDataSource.getTransactions()
val transactions = entities.map { it.toTransaction() }
return transactions
}
override suspend fun getInvestments(): List<Investment> {
val entities = localDataSource.getInvestments()
val investments = entities.map { it.toInvestment() }
return investments
}
}
What’s Next
In the next part, you will:
- Replace in-memory database with Room
- Add ViewModel and state management
- Implement dependency injection
- Prepare app for Play Store release
If you want more real-world Android projects like this, make sure to follow the series. Each part builds on the previous one and helps you become a better Android developer.