As a personal finance ledger grows from fifty transactions to five thousand, finding specific financial events becomes a needle-in-a-haystack problem.
A simple search bar can match a merchant name like "Amazon", but text search alone cannot answer multidimensional financial questions:
- "Show me all credit card purchases between 1,000 and 5,000 rupees during our trip to Goa last August."
- "Show me all cash expenses categorized under Dining across the last three months."
- "Show me all transactions that have receipt photo attachments uploaded."
Answering these questions requires multi-dimensional filtering.
Here is how FinLedger designed and built its multi-dimensional filter bottom sheet (LedgerFilterSheet.kt) in Jetpack Compose, supporting real-time multi-variable constraints without lag.
The Five Dimensions of a Financial Filter
In Sprint 8 (commit 43d9bfb), we formalized the multidimensional filter model into five orthogonal filter axes:
classDiagram
class LedgerFilterState {
+DateRangeFilter dateRange
+Set~Long~ selectedAccountIds
+Set~TransactionType~ selectedTypes
+Set~Long~ selectedCategoryIds
+ClosedFloatingPointRange amountRange
+Boolean hasAttachmentsOnly
+Boolean isDefault()
+Int activeFilterCount()
}
class DateRangeFilter {
ALL_TIME
THIS_MONTH
LAST_MONTH
CUSTOM_RANGE(start, end)
}
LedgerFilterState *-- DateRangeFilter
-
Temporal Dimension (Date Range): All Time, This Month, Last Month, or a custom date range picker (
startDatetoendDate). - Account Dimension: Filter to any combination of specific accounts (e.g. HDFC Checking + Cash Wallet, excluding Credit Cards).
- Transaction Type Dimension: Income, Expense, Transfer, Credit Card Purchase, or Card Repayment.
- Categorical Dimension: Filter across multiple spending categories (e.g. Groceries + Dining Out + Fuel).
- Magnitude Dimension (Amount Slider): A dual-thumb range slider filtering transactions between a minimum and maximum monetary amount (e.g. 500 to 10,000 rupees).
- Attachment Presence: A quick toggle filtering transactions that have attached receipt photos.
The Jetpack Compose Architecture: Bottom Sheet State Hoisting
To provide a smooth user experience, the filter sheet is rendered as a modal bottom sheet using Material 3 ModalBottomSheet.
A critical architectural consideration in Jetpack Compose is state hoisting:
flowchart LR
subgraph StateHoistingFlow["Filter State Hoisting Flow"]
direction LR
VM["LedgerViewModel:
Holds Authoritative _filterState"] --> Sheet["LedgerFilterSheet (Composable):
Maintains Local Draft State"]
Sheet -->|"User Toggles Chips / Adjusts Sliders"| Draft["Local Draft Updates Fluidly"]
Draft -->|"User Taps 'Apply Filters'"| Apply["Emit New State to ViewModel"]
Draft -->|"User Taps 'Reset All'"| Reset["Revert Local Draft to Default"]
end
1. Isolated Draft State
If the bottom sheet modifies the ViewModel's active filter state on every single chip tap or slider drag, the underlying transaction list will continuously re-filter and recompose in the background while the user is still making decisions.
Instead, LedgerFilterSheet creates a local copy (rememberSaveable) of the filter state upon opening. Users can toggle multiple categories, adjust price sliders, and change date ranges with instant local UI responsiveness.
2. Explicit Commit and Reset
-
Apply Filters Button: Commits the local draft state back to
LedgerViewModel._filterState. The bottom sheet dismisses, and the ledger updates in a single unified emission. - Reset All Button: Resets all five dimensions back to default in one tap.
- Active Filter Badge: The filter icon on the ledger top bar displays a numbered badge indicating exactly how many filter dimensions are currently active (e.g. "3" if filtering by Category, Account, and Date Range).
Real-Time Slider Performance with Paisa Snapping
One of the most complex UI components in the sheet is the dual-thumb amount range slider:
- It must handle ranges from 0 rupees to 500,000+ rupees.
- If the slider steps are linear, adjusting small amounts (like 250 rupees) on a 500,000-rupee scale is nearly impossible because 1 millimeter of finger movement jumps by 10,000 rupees.
- FinLedger solves this using a logarithmic scale curve for the slider values: fine-grained 50-rupee steps at lower amounts (under 5,000), expanding to 500-rupee steps at medium amounts, and 5,000-rupee steps at high amounts.
Zero Distortions in Filtered Exports
A major advantage of formalizing LedgerFilterState is seamless export integration:
- When a user applies a filter (e.g. "All Medical expenses in 2026"), and then taps Export CSV or Generate PDF Statement, the export engines read the exact same filtered transaction set.
- The exported vector PDF statement displays the active filter criteria directly in its header, providing an authoritative, filtered financial document ready for tax filing or expense reimbursement.
Key Takeaway
A financial ledger is only as powerful as its query capabilities.
By organizing filters into orthogonal dimensions, isolating local draft state inside a modal bottom sheet, and integrating filtered sets directly into PDF and CSV export pipelines, FinLedger turns years of complex transaction history into an instantly searchable, audit-ready database.
FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.
Explore FinLedger on Google Play: https://play.google.com/store/apps/details?id=com.devanshufinance.app

Top comments (0)