Adam Bandel


Habit Ledger

Aug 2025
Type: android
Code: 4k lines
Files: 40
Active: Aug 2025 — Aug 2025
Stack:
KotlinJetpack ComposeRoom (SQLite)WorkManagerGlance AppWidget
Tags:
productivityhealthmobilepersonal-data

Overview

Habit Ledger is a comprehensive habit and activity tracking Android application built with modern Jetpack Compose. It allows users to track various types of quantitative and qualitative metrics—from daily water intake and exercise to mood, weight, and custom events—all stored locally on device for complete data privacy.

The app supports five distinct tracker types (quantity, counter, event, note, choice), flexible tagging, customizable home screen widgets for at-a-glance tracking, and an automated reminder system. Data can be exported to JSON or backed up incrementally via NDJSON mirroring.

Screenshots

Main tracker list

Quick entry interface

Home screen widget

Entry history

Problem

Most habit tracking apps either require cloud accounts (privacy concern), lack flexibility for different data types, or don’t provide quick-access widgets for frictionless logging. I needed an app that could track everything from binary events (“Did I take my vitamins?”) to accumulating counters (“Calories today”) to measurements (“Weight this morning”)—all with minimal friction and complete local control.

Approach

Built a flexible tracking system with five tracker types covering most personal data logging needs, combined with Glance-based widgets for instant access from the home screen.

Stack

Challenges

val effectiveMillis = minOf(atMillis, nowMillis)
val effectiveLastReset = minOf(latestReset ?: effectiveMillis, nowMillis)
val sum = entryDao.sumDeltasSince(trackerId, effectiveLastReset)

Outcomes

The app provides a flexible, privacy-respecting habit tracking solution with genuinely useful home screen widgets. The counter widget with split-tap controls enables logging meals or activities in seconds without opening the app.

Key technical learnings:

Implementation Notes

The data model uses five entity types with a flexible schema:

enum class TrackerKind { QUANTITY, NOTE, EVENT, COUNTER, CHOICE }
enum class EntryAction { SET, DELTA, RESET }

@Entity
data class Entry(
    val trackerId: Long,
    val timestamp: Long,
    val tzOffsetMinutes: Int,
    val action: EntryAction,
    val valueDouble: Double? = null,  // For QUANTITY, COUNTER deltas
    val valueText: String? = null,    // For CHOICE selections
    val note: String? = null,
    val dayKey: String                // YYYY-MM-DD in local TZ
)

Counter trackers maintain a separate CounterState entity for efficient total lookups, recomputed on delta entries and resets:

@Entity
data class CounterState(
    @PrimaryKey val trackerId: Long,
    val currentTotal: Double = 0.0,
    val lastResetAt: Long? = null
)

The reminder system uses bit masks for weekly day selection (notifWeeklyDaysMask where bits 0-6 = Sun-Sat) and supports skip-if-logged-today behavior for non-intrusive notifications.


Related Posts