Skip to Content
FeaturesHabitsHabits Feature Guide

Habits Feature Guide

Guide to the Habits module — recurring habit scheduling with flexible frequencies, streak tracking, completion management, and reminders.

Overview

The Habits module lets users define recurring habits (daily, weekly, monthly, etc.) anchored to a specific datetime. Each habit tracks a streak, supports subtasks, and fires reminders. Completion state is date-scoped — marking a habit done on a given date does not affect other occurrences. The server computes the nextOccurrence for each habit based on its frequency and anchor datetime.

Architecture

The shared Habit model carries the nextOccurrence() computation logic so all platforms calculate recurrence consistently without a server round-trip.

Data Models

A habit with no explicit HABIT_REMINDER row still gets a “habit starting soon” push when its anchor occurrence falls within UserSettings.defaultHabitReminderOffsetMinutes (default 15, 0 to disable). See Default reminder offsets.

Frequency Enum

ValueRecurrence
DAILYEvery day at the anchor time
WEEKLYSame day of week as the anchor
BI_WEEKLYEvery two weeks, parity-matched to the anchor week
MONTHLYSame day of month, clamped to month-end
YEARLYSame month + day
ONE_TIMENo recurrence — appears once

Next Occurrence Calculation

nextOccurrence(referenceDate) is computed client-side from the anchor dateTime + frequency. Key rules:

  • DAILY: advance anchor by 1 day until ≥ referenceDate
  • WEEKLY: advance by 7 days
  • BI_WEEKLY: advance by 14 days, week-parity preserved
  • MONTHLY: same day of next month, clamped if month is shorter (e.g. Jan 31 → Feb 28)
  • YEARLY: same date next year

API Endpoints

MethodPathDescription
GET/habitsList habits (date range, pagination, withOverdue)
POST/habitsCreate habit (dateTime required, frequency validated)
GET/habits/{id}Get habit detail (date query param required)
PATCH/habits/{id}Update habit
DELETE/habits/{id}Delete habit
PATCH/habits/{id}/completeMark complete for a given dateTime
PATCH/habits/{id}/uncompleteUndo completion for a given dateTime

dateTime parameters use dd/MM/yyyy HH:mm format.

State Machine

Completion is scoped per occurrence date — marking done does not globally flag the habit, only the specific dateTime instance.

Client Behavior

Completion emits a domain event so the Dashboard and Calendar widgets refresh without a manual pull-to-refresh.

Key Files

FilePurpose
shared/.../models/Habit.ktHabit model + nextOccurrence() recurrence logic
shared/.../models/HabitReminder.ktReminder model
habits/habits_domain/.../repository/HabitRepository.ktRepository interface
habits/habits_data/.../repository/HabitRepositoryImpl.ktRepository implementation
habits/habits_presentation/.../viewmodel/HabitViewModel.ktViewModel with MVI state
server/.../routing/HabitRouting.kt7 REST endpoints with date validation

Testing

  • Unit: nextOccurrence() edge cases — month-end clamping, bi-weekly parity, leap years, ONE_TIME habits after their date.
  • Integration: complete/uncomplete toggling per date; verify streak increments correctly on consecutive completions.
  • Server: test withOverdue filtering; habits past their date that are undone should surface when requested.

Troubleshooting

Habit not appearing for today

  • Check that dateTime anchor is in the past or matches today.
  • Verify frequency is not ONE_TIME with a past date.
  • Confirm the startDate/endDate range in the request covers today.

Streak not incrementing

  • Streak only increments when complete is called on consecutive occurrences with no gap.
  • Verify lastDoneAt is being persisted — gaps reset the streak to 1.

Next occurrence wrong

  • Check timezone offset — dateTime is stored and compared in the user’s local timezone.
  • BI_WEEKLY parity is computed from the original anchor date; if the anchor was changed the parity may shift.

API Endpoints

All endpoints require JWT authentication and are prefixed with /api/v1. See server/README.md for full reference.

MethodPathDescription
GET/habitsList habits (filterable by date, projectId, date range)
POST/habitsCreate a habit
GET/habits/{id}Get habit by ID
PATCH/habits/{id}Update habit
DELETE/habits/{id}Delete habit
PATCH/habits/{id}/completeMark habit complete for a datetime
PATCH/habits/{id}/uncompleteMark habit incomplete for a datetime