Habit Loop is a sleek, user-friendly Android application designed to help users build and maintain positive habits. With features like daily streak tracking, personalized reminders, and comprehensive progress visualization, Habit Loop makes habit formation intuitive and rewarding. The app combines modern Android development practices with thoughtful UX design to create a seamless habit tracking experience.
The following entity relationships form the core of Habit Loop:
Habit Loop implements the MVVM architecture pattern, using Jetpack Compose for the UI layer:
@HiltViewModel
class HabitDetailViewModel @Inject constructor(
private val habitRepository: HabitRepository,
private val streakCalculator: StreakCalculator,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val habitId: Long = checkNotNull(savedStateHandle["habitId"])
private val _uiState = MutableStateFlow(HabitDetailUiState())
val uiState: StateFlow<HabitDetailUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
habitRepository.getHabitWithCompletionsFlow(habitId).collect { habitWithCompletions ->
val currentStreak = streakCalculator.calculateCurrentStreak(habitWithCompletions.completions)
val longestStreak = streakCalculator.calculateLongestStreak(habitWithCompletions.completions)
_uiState.update { state ->
state.copy(
habit = habitWithCompletions.habit,
completions = habitWithCompletions.completions,
currentStreak = currentStreak,
longestStreak = longestStreak,
completionRate = calculateCompletionRate(habitWithCompletions)
)
}
}
}
}
fun toggleTodayCompletion() {
viewModelScope.launch {
val today = LocalDate.now()
val hasCompletedToday = uiState.value.completions.any {
it.date == today
}
if (hasCompletedToday) {
habitRepository.deleteCompletionForDate(habitId, today)
} else {
habitRepository.addCompletion(
HabitCompletion(
habitId = habitId,
date = today,
timestamp = System.currentTimeMillis()
)
)
}
}
}
// Additional methods for habit management...
}
Habit Loop uses Room for efficient local data storage:
@Entity(tableName = "habits")
data class HabitEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val description: String,
val categoryId: Long,
val frequency: Int, // Days per week
val reminderTime: Long?, // Epoch timestamp for reminder
val createdAt: Long,
val color: Int,
val iconId: Int,
val isArchived: Boolean = false
)
@Entity(
tableName = "habit_completions",
foreignKeys = [
ForeignKey(
entity = HabitEntity::class,
parentColumns = ["id"],
childColumns = ["habitId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("habitId")]
)
data class HabitCompletionEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val habitId: Long,
val date: LocalDate,
val timestamp: Long
)
@Dao
interface HabitDao {
@Transaction
@Query("SELECT * FROM habits WHERE isArchived = 0 ORDER BY name ASC")
fun getActiveHabitsWithCompletionsFlow(): Flow<List<HabitWithCompletions>>
@Query("SELECT * FROM habits WHERE id = :habitId")
fun getHabitFlow(habitId: Long): Flow<HabitEntity>
@Insert
suspend fun insertHabit(habit: HabitEntity): Long
@Update
suspend fun updateHabit(habit: HabitEntity)
@Query("UPDATE habits SET isArchived = 1 WHERE id = :habitId")
suspend fun archiveHabit(habitId: Long)
// Additional queries for habit management...
}
Habit Loop uses Jetpack Compose for a modern, responsive UI:
@Composable
fun HabitCard(
habit: Habit,
currentStreak: Int,
onCheckClick: () -> Unit,
onCardClick: () -> Unit,
modifier: Modifier = Modifier
) {
val habitColor = Color(habit.color)
Card(
modifier = modifier
.fillMaxWidth()
.clickable { onCardClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(48.dp)
.background(
color = habitColor.copy(alpha = 0.2f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = habit.iconResId),
contentDescription = null,
tint = habitColor,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = habit.name,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Filled.LocalFire,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "$currentStreak day streak",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Checkbox(
checked = habit.completedToday,
onCheckedChange = { onCheckClick() },
colors = CheckboxDefaults.colors(
checkedColor = habitColor
)
)
}
}
}
The app supports seamless theme switching with Material 3 dynamic colors:
@Composable
fun HabitLoopTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
shapes = Shapes,
content = content
)
}
Habit Loop enables users to export their habit data as JSON:
class HabitDataExporter @Inject constructor(
private val habitRepository: HabitRepository,
private val context: Context
) {
suspend fun exportHabitsToJson(uri: Uri): Result<Unit> = withContext(Dispatchers.IO) {
try {
val habits = habitRepository.getAllHabitsWithCompletions()
val habitDtos = habits.map { it.toHabitDto() }
val exportData = ExportData(
version = BuildConfig.VERSION_CODE,
exportDate = System.currentTimeMillis(),
habits = habitDtos
)
val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
val jsonString = json.encodeToString(exportData)
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(jsonString.toByteArray())
} ?: throw IOException("Could not open output stream")
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
Building consistent habits is challenging, especially in today’s distraction-filled world. Habit Loop addresses this challenge by:
Habit Loop transforms the often difficult process of habit formation into an engaging, rewarding experience that helps users make lasting positive changes in their lives.
Habit Loop has received positive feedback from early users, with particular praise for:
The app continues to evolve based on user feedback, with planned enhancements including habit challenges, social sharing options, and advanced analytics.