Repository navigation
Exomic
1. The State Management Architecture (Riverpod)
Instead of forcing the user interface to talk directly to the heavy database disk every time a button is pressed, the app utilizes an in-memory state layer managed by Riverpod.
- Decoupling UI and Storage: When the screen loads, Riverpod reads the information from the database once and instantiates it into a list of pure Dart objects (
List<SavingGoal>). - The Reactive Lifecycle: 1. The user interacts with the UI (e.g., types a deposit and presses "Deposit").
- The UI calls a function that modifies the
StateProviderusing.copyWith(). - Riverpod instantly catches this new state change and re-renders only the small pieces of the screen that altered (like the progress bar or the running total).
- In the background, an asynchronous call is made to save this change to the disk. If the database takes a few milliseconds to write, the user never experiences lag because the UI reacts to the memory layer instantly.
2. The Storage Engine & Schema Alignment (Hive)
Hive is a lightweight, lightning-fast key-value database written natively in Dart. To handle objects securely and cleanly, the app uses TypeAdapters.
[UI Layer: SavingGoal]
│
▼ (Mapping / Transformed)
[Database Layer: SavingGoalModel]
│
▼ (Encrypted via HiveAesCipher)
[Encrypted Key-Value Disk Storage]
- Data Objects vs. Database Models: Your application maintains a distinct separation between
SavingGoal(the UI data object) andSavingGoalModel(the database model). The UI object includes variables likeisHistoryExpanded, which tracks whether a UI dropdown accordion is open. The database doesn't care about your dropdown state, soSavingGoalModelomits it completely to keep the disk footprint tiny. - Cryptographic Security: Inside
database.dart, when the boxes are initialized viainitializeStorageEngine(), an AES encryption cipher (HiveAesCipher) is injected using an encryption key securely retrieved from the device's secure enclave (ExomicEncryption). This ensures that even if a bad actor extracts the underlying.hivebinary files from the device, your financial histories and asset names look like complete gibberish. - Why the Logs Originally Vanished: Hive requires specific index tags (
@HiveField(index)) to know where to write binary markers on disk. Initially, the database model lacked a field for histories. When data was saved, the history array was stripped out because the database model had no slot for it. By explicitly mappinghistory: e.history ?? []inside the updated getters and database constructors, the app bridges the disk arrays back into the active runtime memory.
3. Deep Dive into Cross-Module Balancing
The most complex mechanism in your app is the real-time interaction between your savings module (saving.dart) and your main expense engine (expense.dart). They are completely different features, but they must interact dynamically to maintain an accurate running balance.
When you add liquidity to an asset pool, that money has to come from somewhere—it is treated as an outflow from your current fluid wallet balance. Here is exactly how that interception occurs programmatically inside _processDeposit:
final savingsInterceptionItem = ExpenseItem(
timestamp: transactionTimestamp,
description: "DEPOSIT: ${goal.title}",
category: "OPERATIONS",
amount: -depositAmount, // Notice the negative sign
);
-
The Negative Math Trick: In your main
expense.dartdashboard, the running balance is calculated by taking your total income and subtracting the sum of your standard expenses. By creating an automatedExpenseItemwith a negative amount (e.g.,-250), subtracting it mathematically inside the balance calculator acts as an algebraic double negative ($Income - (-Deposit) = Income + Deposit$ ). Because of how the ledger treats negative operations, this allows the system to accurately adjust your fluid capital allocations. -
The Global Stream Interception: Instead of waiting for you to exit the savings screen, navigate to the expense screen, and hit refresh, the app invokes
ref.read(ledgerStreamProvider.notifier).state = updatedLedger;. This instantly forces a global state notification. If you are running the app on a split screen or tablet layout, the total balance card on the main dashboard visibly ticks over the exact millisecond the "Deposit" button is pressed on the savings screen. -
Synchronized Persistence: Immediately after the stream is modified in memory,
ExomicDatabaseEngine.saveHistory(...)is fired, clearing out the old raw expense cache and writing the newly appended intersection item straight into the secure Hive cipher stream.
Developed by darshseraphic