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.
Master Android Development
Deepen your understanding of Jetpack Compose, Kotlin, and modern mobile architecture in our comprehensive guide.
Final Project Preview
Balance
$1,240.50
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
-
1Master modern Android project architecture.
-
2Implement local data persistence with Room.
-
3Build 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
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
.exeor.dmgand follow the setup wizard.
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.
-
1Select "Standard" installation - it includes the SDK and Emulator optimized for the most common use cases.
-
2Verify SDK Components - ensure "Android SDK Platform" and "Android SDK Build-Tools" are checked.
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.
Action Items
-
1Open Android Studio and click "New Project".
-
2Pick "Empty Compose Activity"
-
3Type
BudgetTrackeras the name and ensure the package matches. -
4Set Minimum SDK to API 24 (Nougat).
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.
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.
@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 → New → Kotlin Class/File.
Name it Expense and select Class.
Paste the code above, replacing the default class definition.
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.
@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.
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.
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
BudgetViewModel.kt.
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.
@Database(entities = [Expense::class], version = 1)
abstract class BudgetDatabase : RoomDatabase() {
abstract val dao: ExpenseDao
}
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.
@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.
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.
@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)
}
}
}
}
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.
var title by remember { mutableStateOf("") }
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Title") }
)
Interaction Details
-
Implement a
TextFieldfor the Title and aDecimalkeyboard for the Amount. -
When the Save button is clicked, call
viewModel.addExpense()and navigate back.
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.
<!-- No special permissions needed for local DB! -->
<application
android:icon="@mipmap/ic_launcher"
android:label="Budget Tracker">
<activity android:name=".MainActivity" />
</application>
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.
-
1Connect your phone via USB and enable USB Debugging in Developer Options.
-
2Select your device from the dropdown menu in the top toolbar.
-
3Click the green Run button (Shift + F10) in Android Studio.