Building an Offline Notes or Expense Tracker Application
Course: Android App Development
Topic: Building an Offline Notes or Expense Tracker Application
Course Link: Android App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction
Building an offline Notes or Expense Tracker application is a practical Android development project that teaches developers how to create applications capable of storing and managing information without an internet connection. These applications use local storage to preserve user data directly on the device.
An offline Notes application allows users to create, view, edit, search, and delete personal notes. An Expense Tracker allows users to record expenses, categorize transactions, monitor spending, and calculate totals. Both applications demonstrate important Android concepts, including user interface design, local database management, data validation, application architecture, and persistent storage.
For this project, developers can use Kotlin with Jetpack Compose for the user interface and Room for local database operations. Room provides an abstraction over SQLite and simplifies database access, query management, and schema validation.
Learning Objectives
- Understand the architecture of an offline Android application.
- Design a user-friendly interface using Jetpack Compose.
- Store application data locally using Room and SQLite.
- Implement Create, Read, Update, and Delete (CRUD) operations.
- Use ViewModel and Kotlin Flow to manage application data and UI state.
- Implement searching, filtering, and sorting.
- Handle validation, errors, and empty states.
- Test offline functionality and data persistence.
2. Understanding Offline Applications
An offline application performs its essential operations without depending on a remote server or continuous internet connectivity. The application stores information locally and reads it from the device whenever required.
How an Offline Application Works
- The user opens the application.
- The application retrieves saved information from its local database.
- The user creates or modifies a note or expense.
- The application validates the entered information.
- The database stores the new or updated record.
- The user interface displays the latest database contents.
- The information remains available after the application is closed and reopened.
Advantages of Offline Applications
| Advantage |
Explanation |
| No internet dependency |
Users can perform essential operations without a network connection. |
| Fast access |
Local data can be retrieved without waiting for a remote server. |
| Privacy |
Data can remain on the device instead of being transmitted to a server. |
| Availability |
Saved information remains accessible when connectivity is unavailable. |
| Reduced network usage |
Routine database operations do not require internet requests. |
Important: Offline storage does not automatically protect data against device loss, application uninstallation, or device failure. Backup and recovery features must be designed separately when required.
3. Choosing the Project: Notes App or Expense Tracker
Both projects use similar technical concepts, but their data models and business rules differ.
| Feature |
Offline Notes App |
Offline Expense Tracker |
| Main purpose |
Save personal information and reminders. |
Track income and expenses. |
| Primary fields |
Title, content, creation date, update date. |
Amount, category, date, payment method, description. |
| Main screen |
List of saved notes. |
Transaction list and spending summary. |
| Search |
Search by title or note content. |
Search by description or category. |
| Reports |
Recent notes and pinned notes. |
Daily, monthly, and category-wise spending. |
| Validation |
Validate required title or content according to app rules. |
Validate positive amounts, categories, and dates. |
4. Technology Stack
The following technologies provide a modern approach to building a local-first Android application.
| Technology |
Purpose |
| Kotlin |
Implements application logic and data models. |
| Android Studio |
Provides the development environment and debugging tools. |
| Jetpack Compose |
Builds screens and reusable UI components. |
| Room |
Manages structured local data using SQLite. |
| ViewModel |
Maintains screen-related state across configuration changes. |
| Kotlin Flow |
Observes database changes and emits updated data. |
| Coroutines |
Supports asynchronous operations without blocking the main thread. |
| Navigation Compose |
Handles navigation between screens when multiple screens are used. |
5. Planning the Application Architecture
A clear architecture separates the user interface from database operations and business logic. This makes the application easier to test, debug, and maintain.
Recommended Architecture
Android Application
|
v
Jetpack Compose UI
|
v
ViewModel
|
v
Repository
|
v
Room DAO
|
v
SQLite Database
|
v
Local Device Storage
Responsibilities of Each Layer
- UI layer: Displays notes or expenses and collects user input.
- ViewModel: Holds screen state and coordinates user actions.
- Repository: Provides a consistent interface for accessing application data.
- DAO: Defines database operations such as inserting, retrieving, updating, and deleting records.
- Room database: Creates and manages the local SQLite database.
This separation prevents the user interface from containing all database logic and makes it easier to change or test individual components.
6. Setting Up the Android Project
Start by creating a new Android project in Android Studio using Kotlin. Choose a Compose-enabled project template and configure the minimum SDK based on the devices your application needs to support.
Basic Setup Steps
- Open Android Studio.
- Create a new project using an appropriate Compose template.
- Select Kotlin as the programming language.
- Configure the project name, package name, and minimum SDK.
- Ensure that Jetpack Compose is enabled.
- Add the required Room, Lifecycle ViewModel, and Kotlin Coroutines dependencies.
- Create packages for data models, database access, repositories, ViewModels, and UI screens.
Suggested Project Structure
com.example.offlinetracker/
data/
Note.kt
NoteDao.kt
AppDatabase.kt
NoteRepository.kt
ui/
NotesScreen.kt
NoteEditorScreen.kt
viewmodel/
NotesViewModel.kt
MainActivity.kt
For an Expense Tracker, you can add an Expense.kt entity, an ExpenseDao.kt interface, and the related screens and ViewModel. The same architectural approach can support either project.
7. Designing the User Interface
The interface should make common actions easy to discover. Users should be able to view saved records, create a new record, edit existing information, and delete unwanted entries.
Notes Application Screens
- Home screen: Displays saved notes in a list or grid.
- Create note screen: Contains fields for the note title and content.
- Edit note screen: Loads an existing note and allows modifications.
- Search interface: Filters notes based on a query.
- Confirmation dialog: Asks for confirmation before permanently deleting a note.
Expense Tracker Screens
- Dashboard: Shows total expenses and useful summaries.
- Add expense screen: Accepts amount, category, date, and description.
- Transaction history: Displays recorded income or expenses.
- Category filter: Shows transactions from a selected category.
- Monthly report: Summarizes spending over a selected period.
Example: Basic Notes Form Using Jetpack Compose
@Composable
fun NoteInputForm(
title: String,
content: String,
onTitleChange: (String) -> Unit,
onContentChange: (String) -> Unit,
onSave: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = title,
onValueChange = onTitleChange,
label = { Text("Note Title") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = content,
onValueChange = onContentChange,
label = { Text("Write your note") },
modifier = Modifier.fillMaxWidth(),
minLines = 5
)
Button(
onClick = onSave,
modifier = Modifier.fillMaxWidth()
) {
Text("Save Note")
}
}
}
This reusable composable receives its values and callbacks from the calling screen. The actual database save should be performed through the ViewModel and repository rather than directly inside the UI component.
8. Designing the Local Database
The database design depends on the type of application. A Notes application generally requires a table for notes, while an Expense Tracker requires a table for transactions and may use additional tables for categories or budgets.
Example Notes Database Fields
| Field |
Suggested Type |
Purpose |
| id |
INTEGER primary key |
Uniquely identifies each note. |
| title |
TEXT |
Stores the note title. |
| content |
TEXT |
Stores the note body. |
| createdAt |
INTEGER |
Stores the creation timestamp. |
| updatedAt |
INTEGER |
Stores the latest update timestamp. |
Example Expense Database Fields
| Field |
Suggested Type |
Purpose |
| id |
INTEGER primary key |
Identifies the transaction. |
| amountMinor |
INTEGER |
Stores the amount in the currency's smallest unit, such as paise. |
| category |
TEXT |
Stores the expense category. |
| description |
TEXT |
Stores transaction details. |
| date |
INTEGER |
Stores the transaction date or timestamp. |
| type |
TEXT |
Identifies income or expense transactions. |
Storing currency as an integer number of minor units avoids many floating-point rounding problems. The application should also define how it handles currencies with different minor-unit conventions.
9. Creating a Note Entity with Room
Room uses entity classes to represent database tables. Each instance of the entity corresponds to a record in the table.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val content: String,
val createdAt: Long,
val updatedAt: Long
)
Explanation:
@Entity marks the class as a Room database entity.
tableName = "notes" defines the database table name.
@PrimaryKey defines the unique identifier.
autoGenerate = true allows Room to generate identifiers for new records.
- The timestamps help the application display creation and modification dates.
10. Creating a Data Access Object (DAO)
A DAO defines how the application reads and modifies data in the database. Room generates the implementation of the DAO based on its annotations.
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
fun getAllNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Long): Note?
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertNote(note: Note): Long
@Update
suspend fun updateNote(note: Note)
@Delete
suspend fun deleteNote(note: Note)
@Query(
"SELECT * FROM notes " +
"WHERE title LIKE '%' || :query || '%' " +
"OR content LIKE '%' || :query || '%' " +
"ORDER BY updatedAt DESC"
)
fun searchNotes(query: String): Flow<List<Note>>
}
The DAO supports common database operations. Returning a Flow allows the UI layer to observe changes in the query results. The suspend functions can be called from a coroutine.
11. Creating the Room Database
The Room database class connects the entity and DAO to the underlying SQLite database.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(
entities = [Note::class],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"offline_notes_database"
).build().also {
INSTANCE = it
}
}
}
}
}
This example creates a singleton database instance so that different parts of the application can share the same database. Production projects should configure schema export and versioned migrations as the data model evolves.
12. Implementing the Repository Layer
The repository separates the application's data access logic from its ViewModel. It exposes operations that the ViewModel can call without needing to manage database details directly.
class NoteRepository(
private val noteDao: NoteDao
) {
val allNotes = noteDao.getAllNotes()
suspend fun addNote(note: Note): Long {
return noteDao.insertNote(note)
}
suspend fun updateNote(note: Note) {
noteDao.updateNote(note)
}
suspend fun deleteNote(note: Note) {
noteDao.deleteNote(note)
}
fun searchNotes(query: String) =
noteDao.searchNotes(query)
}
The repository is a useful place to combine multiple data sources in a larger application. For a simple offline project, it may use only the Room DAO.
13. Managing UI State with ViewModel
The ViewModel coordinates user actions and database operations. It exposes observable data to the UI and uses coroutines to perform operations asynchronously.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class NotesViewModel(
private val repository: NoteRepository
) : ViewModel() {
val notes = repository.allNotes.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun saveNote(
title: String,
content: String
) {
val cleanTitle = title.trim()
val cleanContent = content.trim()
if (cleanTitle.isBlank() && cleanContent.isBlank()) {
return
}
val now = System.currentTimeMillis()
val note = Note(
title = cleanTitle.ifBlank { "Untitled Note" },
content = cleanContent,
createdAt = now,
updatedAt = now
)
viewModelScope.launch {
repository.addNote(note)
}
}
fun deleteNote(note: Note) {
viewModelScope.launch {
repository.deleteNote(note)
}
}
}
This is a basic example for creating and deleting notes. An edit operation should preserve the original creation timestamp and update only the modification timestamp. A production application should also expose validation and database errors to the UI rather than silently ignoring every failure.
14. Building the Notes List Screen
The list screen displays saved notes and provides a starting point for opening, editing, or deleting them.
@Composable
fun NotesList(
notes: List<Note>,
onDelete: (Note) -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = notes,
key = { note -> note.id }
) { note ->
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = note.title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = note.content,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
TextButton(
onClick = { onDelete(note) }
) {
Text("Delete")
}
}
}
}
}
}
Observe the ViewModel's state in the parent screen and pass the current note list to this composable. A complete screen should also include loading, empty, error, and confirmation states where appropriate.
15. Building an Offline Expense Tracker
An Expense Tracker extends the same local persistence architecture with financial records and calculations. The application should support recording transactions and calculating summaries without requesting data from a server.
Example Expense Entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "expenses")
data class Expense(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val amountMinor: Long,
val category: String,
val description: String,
val date: Long,
val type: String = "EXPENSE"
)
Here, amountMinor represents the amount in the currency's smallest unit. The application should validate that the amount is positive and that the transaction type is one of the allowed values.
Expense DAO
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import androidx.room.Delete
import kotlinx.coroutines.flow.Flow
@Dao
interface ExpenseDao {
@Query("SELECT * FROM expenses ORDER BY date DESC")
fun getAllExpenses(): Flow<List<Expense>>
@Insert
suspend fun insertExpense(expense: Expense): Long
@Update
suspend fun updateExpense(expense: Expense)
@Delete
suspend fun deleteExpense(expense: Expense)
@Query(
"SELECT COALESCE(SUM(amountMinor), 0) " +
"FROM expenses WHERE type = 'EXPENSE'"
)
fun getTotalExpenses(): Flow<Long>
}
Add Expense::class to the Room database's entity list and expose an expenseDao() method before using this DAO. When calculating monthly totals, filter by a defined date range rather than summing all transactions.
16. Calculating Expense Summaries
An Expense Tracker can display total spending, category totals, and monthly summaries. Calculations should be based on the transaction type and date range.
Example SQL for Monthly Expenses
SELECT COALESCE(SUM(amountMinor), 0)
FROM expenses
WHERE type = 'EXPENSE'
AND date >= :startDate
AND date < :endDate;
The start and end values define a date interval, with the end excluded. Calculate these boundaries consistently using the user's intended calendar and time zone.
Suggested Dashboard Metrics
- Total expenses for the current month.
- Total income for the current month.
- Difference between income and expenses.
- Highest-spending category.
- Recent transactions.
- Comparison between the current month and previous months.
For a simple offline project, these summaries can be computed directly from Room queries. More advanced projects can add budget limits, recurring transactions, charts, and export functionality.
17. Implementing Search and Filtering
Search and filtering help users locate records in larger collections. Notes can be searched by title or content, while expenses can be filtered by date, category, transaction type, or amount range.
Notes Search
@Query(
"SELECT * FROM notes " +
"WHERE title LIKE '%' || :query || '%' " +
"OR content LIKE '%' || :query || '%' " +
"ORDER BY updatedAt DESC"
)
fun searchNotes(query: String): Flow<List<Note>>
Expense Category Filter
@Query(
"SELECT * FROM expenses " +
"WHERE category = :category " +
"ORDER BY date DESC"
)
fun getExpensesByCategory(
category: String
): Flow<List<Expense>>
Search behavior should be tested with empty input, different capitalization, punctuation, and large datasets. If search becomes expensive, evaluate indexes and query performance based on actual usage.
18. Ensuring Data Persistence
Data persistence means that saved information remains available after the application is closed. Room stores information in an SQLite database file, so records are not lost merely because the application process stops.
How to Test Persistence
- Launch the application.
- Create a note or expense.
- Confirm that the record appears in the list.
- Close the application completely.
- Reopen the application.
- Verify that the saved record is still visible.
- Edit or delete the record and repeat the test.
Do not use in-memory databases for a project that requires persistence across application restarts. Also remember that clearing application data or uninstalling the application can remove its local database, depending on the platform's backup and restore configuration.
19. Handling Validation and Errors
Validation prevents invalid information from being stored. Error handling helps the application recover gracefully when an operation fails.
Notes Application Validation
- Trim unnecessary whitespace from titles and content.
- Prevent completely empty notes if the product requirements prohibit them.
- Limit unusually large note content when appropriate.
- Handle database failures and display a useful error message.
- Confirm destructive delete actions when needed.
Expense Tracker Validation
- Require an amount greater than zero.
- Require a valid category.
- Validate the transaction date.
- Prevent invalid or overflowing amount conversions.
- Restrict transaction types to supported values.
- Display a clear message when an entry cannot be saved.
Example Amount Validation
fun parseAmountToMinorUnits(input: String): Long? {
val amount = input.trim().toBigDecimalOrNull()
?: return null
if (amount.signum() <= 0) {
return null
}
return try {
amount
.movePointRight(2)
.longValueExact()
} catch (exception: ArithmeticException) {
null
}
}
This example converts a positive amount to minor units for a currency with two decimal places. It rejects values that cannot be represented exactly as a whole number of minor units. Currency-specific decimal precision should be configured when supporting multiple currencies.
20. Offline Storage Security and Privacy
Because notes and financial records may contain sensitive information, privacy should be considered during application design.
- Store only the information necessary for the application's purpose.
- Avoid logging note contents, financial details, or other sensitive user data.
- Do not expose the database through insecure file-sharing mechanisms.
- Review Android backup behavior and define appropriate backup rules.
- Use device-level security and authentication where suitable for the application's risk level.
- Consider encryption requirements for particularly sensitive information.
- Keep dependencies updated and avoid unnecessary permissions.
- Explain how users can delete their stored data.
Room does not automatically encrypt the database. If encryption is required, select and maintain an appropriate storage-security solution and manage encryption keys securely.
21. Testing the Application
Testing verifies that the application behaves correctly and preserves information under normal and unexpected conditions.
| Test Case |
Expected Result |
| Create a new note |
The note is stored and appears in the list. |
| Edit a note |
The changes are saved and displayed correctly. |
| Delete a note |
The selected note disappears from the list. |
| Save an empty note |
The application follows its validation rules and shows feedback when required. |
| Record an expense |
The transaction is saved with the correct amount and category. |
| Enter an invalid amount |
The application rejects the input and explains the problem. |
| Restart the application |
Previously saved records remain available. |
| Search for a record |
Matching records appear according to the search rules. |
| Use the app without internet |
All core local operations continue to work. |
| Upgrade the database schema |
Existing records are preserved by the migration. |
22. Common Problems and Solutions
| Problem |
Possible Cause |
Solution |
| Saved data disappears |
In-memory storage, failed inserts, or a different database instance. |
Use a persistent Room database and verify operation results. |
| UI does not refresh |
The screen is not observing the updated data source. |
Expose Room query results as Flow and collect them as UI state. |
| Application freezes |
Long database operations are running on the main thread. |
Use suspend DAO functions and appropriate coroutine execution. |
| Duplicate records appear |
Repeated save actions or missing uniqueness rules. |
Disable repeated submissions when appropriate and define suitable constraints. |
| Expense totals are incorrect |
Incorrect date boundaries, transaction types, or floating-point calculations. |
Use integer minor units and validate date-range and transaction logic. |
| Database migration fails |
Missing migration logic or incompatible schema changes. |
Implement and test migrations for supported database versions. |
| Search returns unexpected results |
Incorrect query conditions or assumptions about text matching. |
Review SQL filters and test empty queries, capitalization, and special characters. |
23. Project Enhancement Ideas
Once the basic application is working, add features to make the project more useful and demonstrate broader Android development skills.
Notes Application Enhancements
- Pin important notes to the top.
- Add labels, colors, or categories.
- Sort notes by creation date or last modification.
- Add a favorite-notes section.
- Provide a share or export feature.
- Add optional app locking for privacy.
- Support text formatting and note attachments.
Expense Tracker Enhancements
- Build category-wise expense charts.
- Allow users to set monthly budgets.
- Add income and recurring transactions.
- Provide date-range and amount filters.
- Export transactions to CSV.
- Display spending trends over time.
- Add optional reminders to record expenses.
Advanced Enhancements for Both Applications
- Use dependency injection to manage repositories and database dependencies.
- Add automated unit and database tests.
- Introduce a backup and restore mechanism.
- Add optional synchronization with a remote server when connectivity becomes available.
- Support accessibility, dynamic font sizes, and dark mode.
Remote synchronization is an optional extension. The core Notes or Expense Tracker functionality should continue to work without it.
24. Practice Exercises
- Create an offline Notes application with title and content fields.
- Store notes using Room and display them using Jetpack Compose.
- Add editing and deletion functionality.
- Implement search by title and content.
- Create an Expense Tracker with amount, category, description, and date fields.
- Calculate the total expenses for the current month.
- Display spending totals grouped by category.
- Validate empty fields and invalid amounts.
- Test data persistence after closing and reopening the application.
- Implement a database migration that adds a new field without deleting existing records.
25. Interview Questions and Answers
Q1. What is an offline Android application?
Answer: An offline Android application performs its essential operations using locally available data without requiring an active internet connection.
Q2. Why is Room used instead of direct SQLite APIs?
Answer: Room provides entities, DAOs, structured query access, compile-time query validation, and migration support, reducing repetitive database code.
Q3. What is a DAO?
Answer: A Data Access Object defines the operations used to insert, retrieve, update, and delete database records.
Q4. What is the purpose of a ViewModel?
Answer: A ViewModel manages screen-related state and coordinates business operations without placing all application logic inside UI components.
Q5. How does Flow help an offline application?
Answer: Flow can emit updated database query results so the UI can react when saved records change.
Q6. How can data persist after the application closes?
Answer: Store records in a persistent local database such as SQLite through Room rather than keeping them only in memory.
Q7. How should an Expense Tracker represent monetary values?
Answer: A common approach is to store amounts as integer minor units, such as paise, to avoid floating-point rounding issues.
Q8. How can database operations avoid blocking the UI?
Answer: Use asynchronous execution with Kotlin coroutines and suspend DAO functions, and avoid long-running operations on the main thread.
Q9. What happens when a Room database schema changes?
Answer: The database version and migration logic must be updated so existing data can be transformed to the new schema safely.
Q10. How can you verify that an app works offline?
Answer: Disable network connectivity and test creating, reading, updating, deleting, and searching records. Then restart the application and verify that saved information remains available.
26. Quick Revision Checklist
- Understand the requirements of an offline application.
- Design the UI for creating, viewing, editing, and deleting records.
- Use Kotlin and Jetpack Compose for application development.
- Define Room entities and DAOs.
- Use a persistent SQLite database.
- Separate UI, ViewModel, repository, and database responsibilities.
- Observe database changes using Flow.
- Validate note content and expense amounts.
- Implement searching, filtering, and summary calculations.
- Test offline operation and data persistence.
- Plan schema migrations and protect sensitive information.
27. Conclusion
Building an Offline Notes or Expense Tracker application provides practical experience with essential Android development concepts. The project combines user interface design, Kotlin programming, local database management, state handling, input validation, and application testing.
A Notes application focuses on organizing text-based information, while an Expense Tracker introduces financial calculations, transaction categories, and reporting. Both can use the same architectural foundation with Jetpack Compose, ViewModel, Kotlin Flow, Room, and SQLite.
By completing this project, developers gain experience building applications that remain useful without internet connectivity while learning how to maintain persistent, structured, and reliable local data.
Learn Android App Development with JustAcademy
Explore Android development concepts and build practical mobile applications through structured learning and hands-on projects.
Explore the Android App Development Course
Register for a Course Demo
Visit the JustAcademy Website