1. Why choose Room SQLite for offline-first expense tracking?
SpendWise is an offline-first Android expense tracking application built with Kotlin, Jetpack Compose, and Room SQLite database that stores financial data locally without requiring internet connectivity. In modern mobile app development, relying solely on cloud backend servers poses privacy risks and network latency friction. Financial management apps in particular handle sensitive user transactions where data exposure or network dropped packets can degrade user trust.
To address these concerns, I developed SpendWise, a personal finance application engineered as an offline-first Android app. The app stores all transaction records, budget configurations, and categories locally on the user's device. In this article, I discuss the structural design of SpendWise, focusing on the Android Room SQLite database mapping, SQL transactional queries, Jetpack Compose layouts, and offline PDF document generation.
2. Offline Architecture Strategy
The architectural design of an offline-first mobile app centers on data persistence and fast retrieval. The app must execute database operations seamlessly without blocking the main UI thread. For SpendWise, the architecture utilizes three key layers:
- The Room Database Layer: Binds SQLite operations directly to Kotlin classes. It utilizes DAOs (Data Access Objects) to execute SQL scripts asynchronously.
- Kotlin Coroutines & Flow: Handles non-blocking database queries and exposes live transaction streams.
- Jetpack Compose View Layer: Observes database changes and updates Material 3 dashboard components automatically.
3. Setting Up Room Database: Entities and Relations
The heart of SpendWise is its local SQL database, created using Google's Room abstraction library. Room provides a compile-time check on SQL syntax, eliminating runtime query crashes. Our database schema models two primary entities: Expense and Category.
Let's inspect the Kotlin implementation of the Expense entity:
// Room Database entity representing a transaction
@Entity(
tableName = "expenses",
foreignKeys = [
ForeignKey(
entity = Category::class,
parentColumns = ["id"],
childColumns = ["categoryId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["categoryId"])]
)
data class Expense(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val amount: Double,
val timestamp: Long,
val categoryId: Long,
val note: String? = null
)
By utilizing a foreign key constraint linked to the `Category` table, Room enforces database integrity, ensuring no transaction is orphaned if an associated expense category is deleted.
4. Implementing Data Access Objects (DAOs)
To interact with these tables, we define Data Access Objects. DAOs isolate raw SQL from repository classes. To make the database reactive, we return Kotlin Flow streams. Whenever an expense transaction is added, updated, or removed, Room automatically recalculates the database result and emits the new data to the flow listeners:
// Data Access Object managing SQLite operations
@Dao
interface ExpenseDao {
@Query("SELECT * FROM expenses ORDER BY timestamp DESC")
fun getAllExpenses(): Flow<List<Expense>>
@Query("SELECT SUM(amount) FROM expenses WHERE timestamp >= :startDate")
fun getTotalSpentSince(startDate: Long): Flow<Double?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertExpense(expense: Expense): Long
@Delete
suspend fun deleteExpense(expense: Expense)
}
5. Declarative UI Design with Jetpack Compose & Material 3
With the database and repository layers configured, we built a modern dashboard layout using Jetpack Compose. Compose represents a paradigm shift in Android design, replacing traditional XML files with declarative Kotlin code.
In SpendWise, the dashboard reads the Flow emission using the Compose utility collectAsStateWithLifecycle(). The UI dynamically recomposes whenever the database updates. For layout components, we leveraged Google's Material 3 guidelines, implementing beautiful card components with subtle surface elevations and high-contrast color highlights to reflect financial health.
6. Offline PDF Report Generation
A key feature of SpendWise is its offline document printing capability. Users can export structured monthly summaries without an active network connection. I achieved this using the Android framework's native PdfDocument class:
// Generate structured PDF document locally
fun generateMonthlyPdfReport(context: Context, expenses: List<Expense>, fileName: String) {
val pdfDocument = android.graphics.pdf.PdfDocument()
val pageInfo = android.graphics.pdf.PdfDocument.PageInfo.Builder(595, 842, 1).create() // A4 Size
val page = pdfDocument.startPage(pageInfo)
val canvas = page.canvas
val paint = android.graphics.Paint()
// Title Drawing
paint.textSize = 20f
paint.isFakeBoldText = true
canvas.drawText("SpendWise - Monthly Expense Report", 50f, 50f, paint)
// Transaction Loop
paint.textSize = 12f
paint.isFakeBoldText = false
var yPosition = 100f
expenses.forEach { exp ->
canvas.drawText("${exp.title}: $${exp.amount}", 50f, yPosition, paint)
yPosition += 25f
}
pdfDocument.finishPage(page)
val outputFile = File(context.getExternalFilesDir(null), fileName)
pdfDocument.writeTo(FileOutputStream(outputFile))
pdfDocument.close()
}
This implementation creates an A4 document with complete formatting and saves it locally in the application's file space. The user can print or share it using standard system shares without any cloud processing dependency.
7. Conclusion & Next Steps
SpendWise demonstrates that local-first Android architectures using Kotlin, Jetpack Compose, and Room provide a fast, secure, and privacy-respecting alternative to cloud-dependent apps. Isolating database calls behind Repository patterns ensures the code is maintainable, testable, and robust.
If you are looking to build a clean local database app or modernize your current view layouts using Jetpack Compose, please explore my featured projects on the Muhammad Saad portfolio home page, or submit an inquiry directly through the contact form.