Build Investment Tracker App in Jetpack Compose (Clean Architecture + Full Tutorial)

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.

This tutorial is for educational purposes. You can extend this app for real-world use, but you are responsible for how you implement and deploy it.

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.

Post a Comment

Share your thoughts ...

Previous Post Next Post