About the
Lab.

Welcome to the Budget Tracker Lab. In this session, we'll dive deep into Android development and build a sleek, functional expense management app from the ground up.

Recommended Learning

Master Android Development

Deepen your understanding of Jetpack Compose, Kotlin, and modern mobile architecture in our comprehensive guide.

Start Learning

Final Project Preview

9:41

Balance

$1,240.50

Starbucks Coffee
Food & Drink
-$5.50
Google Cloud
Utilities
-$12.00
What we are building

We'll be constructing a Personal Finance Tracker using Kotlin and Jetpack Compose. You'll learn to handle persistent data with Room, manage state with ViewModels, and create modern Android interfaces that feel premium and responsive.

Lab Objectives

  • 1
    Master modern Android project architecture.
  • 2
    Implement local data persistence with Room.
  • 3
    Build reactive UIs that update in real-time.

Getting the Tools

Every app begins with a solid environment. Our tool of choice is Android Studio, the official IDE provided by Google.

Official Source

developer.android.com/studio

Download the Panda 1 stable build.

How to do it

  • Click the link above to visit the official portal.
  • Download the installer for your OS (Windows/Mac/Linux).
  • Run the .exe or .dmg and follow the setup wizard.
Why this tool?

Android Studio isn't just a text editor; it's a compiler, an emulator, and a layout engine. It translates your Kotlin code into bytecode that the Android Runtime (ART) can understand.

Environment Setup

Launching Android Studio for the first time is a one-time configuration process.

Welcome to Android Studio + New Project Open Existing Project Clone Repository
  • 1
    Select "Standard" installation - it includes the SDK and Emulator optimized for the most common use cases.
  • 2
    Verify SDK Components - ensure "Android SDK Platform" and "Android SDK Build-Tools" are checked.
What is the SDK?

The Software Development Kit (SDK) is a massive library containing the core Android classes (like Button, Intent, and Camera). Without it, your code wouldn't know what a "Phone" even is.

Create the Foundation

Let's initialize the project settings.

Template Empty Compose Activity
Project Name BudgetTracker
Package Name com.undrlib.budget

Action Items

  • 1
    Open Android Studio and click "New Project".
  • 2
    Pick "Empty Compose Activity"
  • 3
    Type BudgetTracker as the name and ensure the package matches.
  • 4
    Set Minimum SDK to API 24 (Nougat).
Why API 24?

Choosing a Minimum SDK is a balance. API 24 gives us access to modern features like Multi-window and Direct Boot while still supporting over 95% of active devices globally.

Directory Layout

Understand where your code actually lives.

/java/com.undrlib.budget/

The brain of your app. All .kt files live here, organized by their function.

/res/drawable/

Visual assets. Vectors (SVG) and PNGs that define the look and feel.

Why this hierarchy?

Android uses a Module-based system. The app folder is your main module. This separation allows you to build different versions of the app (e.g., Free vs Pro) while reusing the same core code.

Modeling the Data

First, we define what an "Expense" looks like in our database.

Expense.kt
@Entity(tableName = "expenses")
data class Expense(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val title: String,
    val amount: Double,
    val category: String,
    val date: Long = System.currentTimeMillis()
)

In your Project pane, right-click your package name → NewKotlin Class/File.

Name it Expense and select Class.

Paste the code above, replacing the default class definition.

Why @Entity?

We use Room Persistence Library. The @Entity annotation tells Room to create a table in the SQLite database specifically for this class. Data classes are used because they are designed purely to hold data.

Database Access

We need an interface to talk to our database.

ExpenseDao.kt
@Dao
interface ExpenseDao {
    @Query("SELECT * FROM expenses ORDER BY date DESC")
    fun getAllExpenses(): Flow<List<Expense>>
 
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertExpense(expense: Expense)
}

• Create ExpenseDao.kt in the same folder.

• Note the suspend keyword — it's vital for background threading.

• The Flow return type ensures real-time updates without polling.

Why Coroutines & Flow?

Database operations are "expensive" (slow). suspend tells Android to run this on a background thread so the UI doesn't freeze. Flow allows us to "listen" to changes—every time a new expense is added, the UI will update automatically!

Managing the State

The ViewModel connects the data to the UI.

BudgetViewModel.kt
class BudgetViewModel(dao: ExpenseDao) : ViewModel() {
    val expenses = dao.getAllExpenses()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
    
    fun addExpense(title: String, amt: Double) {
        viewModelScope.launch {
            // Logic to save to DAO...
        }
    }
}

Implementation Checklist

Step 1 Create BudgetViewModel.kt.
Step 2 Extend ViewModel().
Why ViewModel?

If your phone rotates, Android destroys and recreates your Screen (Activity). A ViewModel survives this process. It keeps your data alive so it doesn't disappear when the screen flips.

Database Provider

We must define how to build the actual Room database.

BudgetDatabase.kt
@Database(entities = [Expense::class], version = 1)
abstract class BudgetDatabase : RoomDatabase() {
    abstract val dao: ExpenseDao
}
Why an Abstract Class?

Room is a code generator. When you hit Build, Room will look at this abstract class and generate the underlying implementation for you. It's the "master brain" that manages all your database tables.

Building the UI

First, we create a list item for each expense.

MainScreen.kt
@Composable
fun ExpenseRow(item: Expense) {
    Card(
        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
        colors = CardDefaults.cardColors(containerColor = Color.White)
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = item.title, style = MaterialTheme.typography.titleMedium)
                Text(text = item.category.uppercase(), style = MaterialTheme.typography.labelSmall, color = Color.Gray)
            }
            Text(
                text = "-$${item.amount}", 
                style = MaterialTheme.typography.titleMedium,
                color = Color(0xFFEF4444), // Red-500
                fontWeight = FontWeight.Bold
            )
        }
    }
}

Design Instructions

Create a new file MainScreen.kt. This will be the home for all our UI components. Inside, define the ExpenseRow function exactly as shown. Notice how we use MaterialTheme to keep our typography consistent with Google's design standards.

Why Modifiers?

In Compose, Modifiers are the only way to decorate or augment a component. Modifier.weight(1f) is magical—it tells the title to suck up all available space, pushing the amount to the far right.

The Smart List

Efficiently rendering 100s of items.

MainScreen.kt
@Composable
fun SummaryCard(balance: Double) {
    Card(
        modifier = Modifier.fillMaxWidth().padding(16.dp),
        colors = CardDefaults.cardColors(containerColor = Color.White)
    ) {
        Column(modifier = Modifier.padding(24.dp)) {
            Text("BALANCE", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
            Text(text = "$${balance}", style = MaterialTheme.typography.headlineLarge)
        }
    }
}

@Composable
fun BudgetApp(viewModel: BudgetViewModel) {
    val expenses by viewModel.expenses.collectAsState()
    
    // 1. Use Scaffold for standard layout slots
    Scaffold(
        floatingActionButton = {
            FloatingActionButton(
                onClick = { /* Navigate */ },
                containerColor = Color(0xFF3DDC84)
            ) { Icon(Icons.Default.Add, contentDescription = null) }
        }
    ) { padding ->
        // 2. LazyColumn handles massive lists efficiently
        LazyColumn(contentPadding = padding) {
            items(expenses) { expense ->
                ExpenseRow(expense)
            }
        }
    }
}
        LazyColumn(modifier = Modifier.padding(padding)) {
            item { SummaryCard(balance = 1240.50) }
            items(expenses) { expense ->
                ExpenseRow(expense)
            }
        }
    }
}
Why LazyColumn?

A standard Column tries to render every item at once. LazyColumn only renders what's visible on the screen. This is why apps like Instagram can scroll through infinite photos without crashing.

Capturing Input

Use TextFields to let users add their spending.

AddExpenseScreen.kt
var title by remember { mutableStateOf("") }
OutlinedTextField(
    value = title,
    onValueChange = { title = it },
    label = { Text("Title") }
)

Interaction Details

  • Implement a TextField for the Title and a Decimal keyboard for the Amount.
  • When the Save button is clicked, call viewModel.addExpense() and navigate back.
Why "Remember"?

Compose re-runs your functions constantly (Recomposition). remember tells the system: "Don't forget this value when you re-run the code!". Without it, the text you type would be erased every time a letter is added.

The Manifest

Registering your app with the system.

AndroidManifest.xml
<!-- No special permissions needed for local DB! -->
<application
    android:icon="@mipmap/ic_launcher"
    android:label="Budget Tracker">
    <activity android:name=".MainActivity" />
</application>
Why Local Storage?

Because our Budget Tracker uses SQLite (via Room), we don't need the INTERNET permission. This makes the app faster, offline-capable, and more private for the user's sensitive financial data.

Final Preview

Testing on your physical device.

BALANCE $1,240.50 Starbucks Coffee -$5.50 FOOD & DRINK Google Cloud Subscription -$12.00 UTILITIES
  1. 1
    Connect your phone via USB and enable USB Debugging in Developer Options.
  2. 2
    Select your device from the dropdown menu in the top toolbar.
  3. 3
    Click the green Run button (Shift + F10) in Android Studio.