-
-
Notifications
You must be signed in to change notification settings - Fork 49
🔧 reorganisation #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🔧 reorganisation #134
Conversation
Signed-off-by: shub39 <[email protected]>
WalkthroughRenames several top-level screen composables to Graph variants, moves many files from plural Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant MainActivity
participant Grit as GritNavHost
participant TasksGraph
participant HabitsGraph
participant SettingsGraph
MainActivity->>Grit: initialize(app state, startDestination)
Grit->>Grit: calculate padding (start/end via LocalLayoutDirection, bottom)
Note right of Grit #DDFFDD: Bottom bar uses plain NavigationBar (no rounded clip)
alt Navigate → Tasks
Grit->>TasksGraph: render(state, onAction, nav callbacks)
else Navigate → Habits
Grit->>HabitsGraph: render(state, onAction)
else Navigate → Settings
Grit->>SettingsGraph: render(state, onAction)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (1)
80-89: Bug: Random colors will change on every recomposition (flicker).
Colors for each habit should be stable. Seed the RNG per habit and remember the lines.- val weeklyGraphData = state.habitsWithStatuses.entries.map { entry -> - val habit = entry.key - val statuses = entry.value - val color = Color( - red = Random.nextFloat(), - green = Random.nextFloat(), - blue = Random.nextFloat() - ).harmonize(primary, true).fixIfDisliked() - - Line( - label = habit.title, - values = prepareLineChartData(state.startingDay, statuses), - color = SolidColor(color), + val weeklyGraphData = remember(state.habitsWithStatuses.keys.toList(), primary) { + state.habitsWithStatuses.entries.map { (habit, statuses) -> + val seed = habit.hashCode() // or use habit.id hash if available + val rnd = Random(seed) + val color = Color( + red = rnd.nextFloat(), + green = rnd.nextFloat(), + blue = rnd.nextFloat() + ).harmonize(primary, true).fixIfDisliked() + + Line( + label = habit.title, + values = prepareLineChartData(state.startingDay, statuses), + color = SolidColor(color), dotProperties = DotProperties( enabled = false, color = SolidColor(color), strokeWidth = 4.dp, radius = 7.dp ), firstGradientFillColor = color.copy(alpha = 0.8f), secondGradientFillColor = Color.Transparent, popupProperties = PopupProperties( enabled = false ), drawStyle = DrawStyle.Stroke( width = 3.dp, strokeStyle = StrokeStyle.Normal ) - ) - } + ) + } + }app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)
281-299: Description field validates the wrong variable.Both the label and isError for the description OutlinedTextField reference newHabitName instead of newHabitDescription. This breaks validation and UX.
OutlinedTextField( value = newHabitDescription, shape = MaterialTheme.shapes.medium, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Done ), onValueChange = { newHabitDescription = it }, label = { - if (newHabitName.length <= 50) { + if (newHabitDescription.length <= 50) { Text(text = stringResource(id = R.string.description)) } else { Text(text = stringResource(id = R.string.too_long)) } }, - isError = newHabitName.length > 50 + isError = newHabitDescription.length > 50 )app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (2)
155-161: Likely missing import for draggableHandle extension (build break risk)Modifier.draggableHandle() is provided by sh.calvin.reorderable.draggableHandle and needs an explicit import. Without it, this won’t compile.
Apply this import:
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.draggableHandle
154-162: Avoid “clickable but does nothing” drag handle for accessibilityUsing IconButton with onClick = {} creates a focusable/clickable target that performs no action. Prefer a plain Icon (or non-clickable container) with the draggable handle modifier.
Apply this diff:
- IconButton( - modifier = Modifier.draggableHandle(), - onClick = {} - ) { - Icon( - painter = painterResource(R.drawable.baseline_drag_indicator_24), - contentDescription = null - ) - } + Icon( + painter = painterResource(R.drawable.baseline_drag_indicator_24), + contentDescription = null, + modifier = Modifier.draggableHandle() + )app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (3)
119-121: Avoid force-unwrapping (!!) — handle missing habit gracefullycurrentHabit/statuses use !! which will crash if analyticsHabitId is absent or stale. Prefer a safe lookup and early return.
Apply this diff:
- val currentHabit = state.habitsWithStatuses.keys.find { it.id == state.analyticsHabitId }!! - val statuses = state.habitsWithStatuses[currentHabit]!! + val currentHabit = state.habitsWithStatuses.keys.firstOrNull { it.id == state.analyticsHabitId } + ?: return@PageFill + val statuses = state.habitsWithStatuses[currentHabit].orEmpty()
122-126: Derived UI data stored in plain vars and updated in LaunchedEffect will not recomposelineChartData/weekDayData/currentStreak/bestStreak are plain locals; updating them inside LaunchedEffect doesn’t trigger recomposition. Use remember with keys instead and drop the effect.
Apply this diff:
- var lineChartData = prepareLineChartData(state.startingDay, statuses) - var weekDayData = prepareWeekDayData(statuses.map { it.date }, primary) - var currentStreak = countCurrentStreak(statuses.map { it.date }) - var bestStreak = countBestStreak(statuses.map { it.date }) + val lineChartData = remember(statuses, state.startingDay) { + prepareLineChartData(state.startingDay, statuses) + } + val weekDayData = remember(statuses, primary) { + prepareWeekDayData(statuses.map { it.date }, primary) + } + val currentStreak = remember(statuses) { countCurrentStreak(statuses.map { it.date }) } + val bestStreak = remember(statuses) { countBestStreak(statuses.map { it.date }) } @@ - LaunchedEffect(statuses) { - lineChartData = prepareLineChartData(state.startingDay, statuses) - currentStreak = countCurrentStreak(statuses.map { it.date }) - bestStreak = countBestStreak(statuses.map { it.date }) - weekDayData = prepareWeekDayData(statuses.map { it.date }, primary) - } + // Derived values above will recompute when their keys change; no effect needed.Also applies to: 143-148
505-513: Nullabletodayleaks into non-null usages — comparison/ChronoUnit calls won’t compileThe helper composables accept
today: LocalDate?but then use it in comparisons and ChronoUnit without null checks. Maketodaynon-null in signatures (it’s always non-null at call site).Apply this diff:
-private fun CalendarMap( +private fun CalendarMap( state: HabitPageState, onAction: (HabitsPageAction) -> Unit, calendarState: CalendarState, statuses: List<HabitStatus>, - today: LocalDate?, + today: LocalDate, currentHabit: Habit, primary: Color ) {-private fun WeeklyBooleanHeatMap( +private fun WeeklyBooleanHeatMap( heatMapState: HeatMapCalendarState, statuses: List<HabitStatus>, - today: LocalDate?, + today: LocalDate, onAction: (HabitsPageAction) -> Unit, currentHabit: Habit, primary: Color ) {-private fun HabitStats( +private fun HabitStats( currentHabit: Habit, - today: LocalDate?, + today: LocalDate, currentStreak: Int, bestStreak: Int ) {Also applies to: 664-672, 772-777
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt (1)
115-123: Compile error: use Icons.AutoMirrored.Filled.ArrowBack (not Default).The imported symbol is androidx.compose.material.icons.automirrored.filled.ArrowBack; the correct usage is Icons.AutoMirrored.Filled.ArrowBack.
- Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "Navigate Back" - ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Navigate Back" + )app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
224-228: Reorder callback applies moves to the wrong source list.Using tasks.toMutableList() discards prior in-drag mutations and can produce incorrect final order. Apply moves to reorderableTasks instead.
- rememberReorderableLazyListState(lazyListState) { from, to -> - reorderableTasks = tasks.toMutableList().apply { - add(to.index, removeAt(from.index)) - } - } + rememberReorderableLazyListState(lazyListState) { from, to -> + reorderableTasks = reorderableTasks.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }
592-600: Bug: incorrect index when adding to a different category.Index is derived from the current category, not the selected newTaskCategoryId. This yields wrong ordering when adding to another category from the sheet.
- index = state.tasks[state.currentCategory]?.size ?: 0 + index = state.tasks.entries + .firstOrNull { it.key.id == newTaskCategoryId } + ?.value?.size ?: 0
🧹 Nitpick comments (36)
app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt (2)
13-13: Nit: add a trailing newline at EOF.Keeps diffs cleaner and satisfies common linters/ktlint rules.
9-12: Guard against in-place mutations of collections in @immutable state.Because Map/List are read-only views, accidental in-place mutation of their backing data won’t be detected by Compose. Ensure producers always replace these with new instances (copy-on-write) rather than mutating in place. If this has bitten us before, consider persistent collections (kotlinx.collections.immutable) as a follow-up.
Would you like me to scan call sites to detect any
.add,.remove, or casting-to-mutable on these properties?app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
11-13: Fix extra space and make PageFill more reusable (modifier + BoxScope).
- Remove the double space after
fun.- Expose
modifierandalignmentso callers can customize without wrapping.- Accept
BoxScope.() -> Unitto enable alignment APIs inside the slot.Apply this diff:
@@ -// Wrapper to center everything +// Wrapper to center everything @Composable -fun PageFill( - content: @Composable () -> Unit -) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { content() } +fun PageFill( + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + content: @Composable androidx.compose.foundation.layout.BoxScope.() -> Unit +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = alignment, + content = content + ) }app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (2)
27-28: Remove unused experimental opt-ins.This composable doesn’t reference APIs gated by
ExperimentalFoundationApiorExperimentalMaterial3ExpressiveApi. Keeping the opt-in widens the experimental surface area unnecessarily.Apply this diff:
-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun TaskCard(And remove the corresponding imports:
-import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
29-35: Add sensible defaults for modifier/shape and make reorderIcon optional.Improves ergonomics and reduces boilerplate at call sites without breaking existing usages.
Apply this diff:
fun TaskCard( task: Task, dragState: Boolean = false, - reorderIcon: @Composable () -> Unit, - shape: Shape, - modifier: Modifier + reorderIcon: @Composable () -> Unit = {}, + shape: Shape = CardDefaults.shape, + modifier: Modifier = Modifier )app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)
46-67: Provide a graceful pre-Android 12 (API < 31) fallback for the gated preview.On older devices the content area is blank under the “Unlock Plus” button. Consider showing the content with an overlay scrim so users understand what’s being gated.
Apply this diff:
if (!isUserSubscribed) { Box( contentAlignment = Alignment.Center, modifier = Modifier .height(200.dp) .fillMaxWidth() ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - Box( - modifier = Modifier - .matchParentSize() - .blur( - radius = 10.dp, - edgeTreatment = BlurredEdgeTreatment.Unbounded - ) - ) { content() } - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Box( + modifier = Modifier + .matchParentSize() + .blur( + radius = 10.dp, + edgeTreatment = BlurredEdgeTreatment.Unbounded + ) + ) { content() } + } else { + // Fallback: show content with a dimmed scrim + Box(modifier = Modifier.matchParentSize()) { content() } + Box( + modifier = Modifier + .matchParentSize() + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.85f)) + ) + } Button( onClick = onPlusClick ) { Text(text = stringResource(R.string.unlock_plus)) } }Add missing import if needed:
+import androidx.compose.foundation.backgroundapp/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt (2)
1-1: Typo in filename: rename to InitialLoading.kt.Avoids confusion when searching and keeps naming consistent with the composable.
I can open a follow-up PR to rename the file if desired.
15-23: Allow modifier to be passed in for reuse and testability.Enables callers to set size, background, or testTag without wrapping this composable.
Apply this diff:
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun InitialLoading() { - Box( - modifier = Modifier +fun InitialLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center ) { ContainedLoadingIndicator() } }app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (3)
172-188: Reduce O(n) scans in day rendering by precomputing a date set.
dayContent currently scans statusList 3x per day cell. Precompute a HashSet for O(1) lookups.@@ - val weekState = rememberWeekCalendarState( + val weekState = rememberWeekCalendarState( startDate = habit.time.toLocalDate().minusMonths(12), endDate = LocalDate.now(), firstVisibleWeekDate = LocalDate.now(), firstDayOfWeek = startingDay ) + + // Precompute for O(1) membership checks in the calendar + val doneDates = remember(statusList) { + statusList.map { it.date }.toHashSet() + } @@ - dayContent = { weekDay -> - val done = statusList.any { it.date == weekDay.date } + dayContent = { weekDay -> + val done = weekDay.date in doneDates @@ - val donePrevious = - statusList.any { it.date == weekDay.date.minusDays(1) } - val doneAfter = - statusList.any { it.date == weekDay.date.plusDays(1) } + val donePrevious = weekDay.date.minusDays(1) in doneDates + val doneAfter = weekDay.date.plusDays(1) in doneDates
60-66: Nit: label name mismatch for animateColorAsState.
The debug label says "cardBackground" but this state is for content color.- val cardContent by animateColorAsState( + val cardContent by animateColorAsState( targetValue = when (completed) { true -> MaterialTheme.colorScheme.onPrimaryContainer else -> MaterialTheme.colorScheme.onSurface }, - label = "cardBackground" + label = "cardContent" )
225-229: Localize weekday label instead of truncating enum name.
Using getDisplayName honors locale and proper casing.- Text( - text = weekDay.date.dayOfWeek.toString().take(3), + Text( + text = weekDay.date.dayOfWeek.getDisplayName( + java.time.format.TextStyle.SHORT, + java.util.Locale.getDefault() + ),Add imports if not present:
import java.time.format.TextStyle import java.util.Localeapp/src/main/java/com/shub39/grit/habits/presentation/util.kt (2)
15-25: Fix naming for consistency: habitStatuses.
Minor readability improvement; positional call sites remain unaffected.-fun prepareLineChartData( - firstDay: DayOfWeek, - habitstatuses: List<HabitStatus> -): List<Double> { +fun prepareLineChartData( + firstDay: DayOfWeek, + habitStatuses: List<HabitStatus> +): List<Double> { @@ - val habitCompletionByWeek = habitstatuses + val habitCompletionByWeek = habitStatuses
42-68: Optionally align weekday bar order with user’s starting day.
This makes the bar chart match the calendar/week configuration.-fun prepareWeekDayData( - dates: List<LocalDate>, - lineColor: Color -): List<Bars> { +fun prepareWeekDayData( + firstDay: DayOfWeek, + dates: List<LocalDate>, + lineColor: Color +): List<Bars> { @@ - val weekdayBars = DayOfWeek.entries.map { dayOfWeek -> + val orderedDays = DayOfWeek.entries.dropWhile { it != firstDay } + + DayOfWeek.entries.takeWhile { it != firstDay } + val weekdayBars = orderedDays.map { dayOfWeek ->Apply at the call site in OverallAnalytics.kt:
- val weeklyBreakdownData = - prepareWeekDayData(state.habitsWithStatuses.values.flatten().map { it.date }, primary) + val weeklyBreakdownData = prepareWeekDayData( + state.startingDay, + state.habitsWithStatuses.values.flatten().map { it.date }, + primary + )app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (1)
77-80: Cache heavy derived datasets with remember.
These recompute on every recomposition; memoize to cut work.- val heatMapData = prepareHeatMapData(state.habitsWithStatuses) - val weeklyBreakdownData = - prepareWeekDayData(state.habitsWithStatuses.values.flatten().map { it.date }, primary) + val heatMapData = remember(state.habitsWithStatuses) { + prepareHeatMapData(state.habitsWithStatuses) + } + val weeklyBreakdownData = remember(state.habitsWithStatuses, primary) { + // If you adopt the firstDay param change, add it here. + prepareWeekDayData( + state.habitsWithStatuses.values.flatten().map { it.date }, + primary + ) + }If you implement the optional signature change suggested in util.kt, call it as:
prepareWeekDayData(state.startingDay, state.habitsWithStatuses.values.flatten().map { it.date }, primary)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt (3)
72-76: Localize icon content descriptions (avoid hardcoded English).Hardcoded contentDescription strings (“Navigate Back”, “Start”, “Done”, “Fail”) hinder i18n and consistency. Prefer stringResource. If keys don’t exist yet, please add them to strings.xml.
- Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "Navigate Back" - ) + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.navigate_back) + ) @@ - ExportState.IDLE -> Icon( - painter = painterResource(R.drawable.round_play_arrow_24), - contentDescription = "Start" - ) + ExportState.IDLE -> Icon( + painter = painterResource(R.drawable.round_play_arrow_24), + contentDescription = stringResource(R.string.start) + ) @@ - ExportState.EXPORTED -> Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Done" - ) + ExportState.EXPORTED -> Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = stringResource(R.string.done) + ) @@ - RestoreState.IDLE -> Icon( - painter = painterResource(R.drawable.round_play_arrow_24), - contentDescription = "Start" - ) + RestoreState.IDLE -> Icon( + painter = painterResource(R.drawable.round_play_arrow_24), + contentDescription = stringResource(R.string.start) + ) @@ - RestoreState.RESTORED -> Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Done" - ) + RestoreState.RESTORED -> Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = stringResource(R.string.done) + ) @@ - RestoreState.FAILURE -> Icon( - imageVector = Icons.Default.Warning, - contentDescription = "Fail" - ) + RestoreState.FAILURE -> Icon( + imageVector = Icons.Default.Warning, + contentDescription = stringResource(R.string.fail) + )Also applies to: 101-105, 112-116, 149-166
96-105: Allow re-export without forcing a manual reset.After a successful export, the button remains disabled (only enabled on IDLE). Consider enabling unless EXPORTING so users can export again in the same session.
- enabled = state.backupState.exportState == ExportState.IDLE + enabled = state.backupState.exportState != ExportState.EXPORTING
52-56: Persist selection across configuration changes; verify persistable URI permissions.If the user selects a file and rotates the device, the remembered Uri resets. Use rememberSaveable. Also, ensure that restore logic takes persistable read permission if the Uri needs to be accessed beyond the immediate call.
+import androidx.compose.runtime.saveable.rememberSaveable @@ - var uri by remember { mutableStateOf<Uri?>(null) } + var uri by rememberSaveable { mutableStateOf<Uri?>(null) }Would you like me to add a call to contentResolver.takePersistableUriPermission() in the ViewModel/onAction handler if long-lived access is required?
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt (1)
42-45: Localize the nav icon content description.Replace the hardcoded “Navigate Back” with a stringResource for i18n consistency.
- Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "Navigate Back" - ) + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.navigate_back) + )app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (4)
368-369: Fix off-by-one: title/description max length is inconsistent with labels.The button enables at <20 and <50 while the labels consider <=20 and <=50 valid. Align to <= for both.
- enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50, + enabled = newHabitName.isNotBlank() && newHabitName.length <= 20 && newHabitDescription.length <= 50,
85-86: Reduce unnecessary resets of the reorder list state.Using remember(state) will reset reorderableHabits on any state change, even unrelated ones. Key it to the derived habits list instead.
- var reorderableHabits by remember(state) { mutableStateOf(habits) } + var reorderableHabits by remember(habits) { mutableStateOf(habits) }
240-241: Stronger duplicate detection (trim + case-insensitive).Prevents accidental duplicates like “Workout” vs “workout” or trailing spaces.
- val isHabitPresent = { state.habitsWithStatuses.any { it.key.title == newHabitName } } + val isHabitPresent = { + val normalized = newHabitName.trim() + state.habitsWithStatuses.any { it.key.title.equals(normalized, ignoreCase = true) } + }Optionally also trim the title when constructing Habit to keep stored data clean.
130-134: A11y: add contentDescription to the reorder handle icon.Toggle button content is currently decorative to TalkBack. Provide a localized description.
- Icon( - painter = painterResource(R.drawable.baseline_reorder_24), - contentDescription = null - ) + Icon( + painter = painterResource(R.drawable.baseline_reorder_24), + contentDescription = stringResource(R.string.reorder_items) + )If the string key doesn’t exist, please add it.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)
109-109: Minor readability nit: name the key param instead of shadowing withitUsing
ittwice (itemsIndexed’s lambda and key lambda) hurts readability.- itemsIndexed(categories, key = { _, it -> it.id }) { index, category -> + itemsIndexed(categories, key = { _, category -> category.id }) { index, category ->app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)
63-69: Localize hardcoded strings for i18n/accessibility“Grit”, “Discord”, and “Github” are hardcoded; prefer stringResource for localization and a11y consistency.
Example:
- Text( - text = "Grit", + Text( + text = stringResource(R.string.app_name), ) ... - onClick = { uriHandler.openUri("https://discord.gg/nxA2hgtEKf") } + onClick = { uriHandler.openUri("https://discord.gg/nxA2hgtEKf") } ) { Icon( - imageVector = FontAwesomeIcons.Brands.Discord, - contentDescription = "Discord", + imageVector = FontAwesomeIcons.Brands.Discord, + contentDescription = stringResource(R.string.discord), ) } ... Icon( - imageVector = FontAwesomeIcons.Brands.Github, - contentDescription = "Github", + imageVector = FontAwesomeIcons.Brands.Github, + contentDescription = stringResource(R.string.github), )Please confirm R.string.app_name/discord/github exist; I can add them if needed.
Also applies to: 73-93, 100-135
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (2)
94-114: Prefer localized content descriptions for iconsHardcoded contentDescription strings (“Grit Plus”, “Navigate”, “Backup”, “About Libraries”) should use stringResource to align with the rest of the screen.
Example:
- contentDescription = "Grit Plus" + contentDescription = stringResource(R.string.grit_plus)Also applies to: 244-263, 266-286, 289-304
60-63: Check biometric check timing across config changesLaunchedEffect(Unit) runs once per composition; if context-dependent availability can change on configuration updates, consider keying on context as well.
- LaunchedEffect(Unit) { + LaunchedEffect(context) { onAction(SettingsAction.OnCheckBiometric(context)) }app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (3)
127-138: Calendar states may not reflectstartingDaychangesrememberHeatMapCalendarState/rememberCalendarState are created once; if
state.startingDaychanges at runtime, the states may not update. Consider keying them onstate.startingDay.- val heatMapState = rememberHeatMapCalendarState( + val heatMapState = remember(state.startingDay) { + rememberHeatMapCalendarState( startMonth = currentMonth.minusMonths(12), endMonth = currentMonth, firstVisibleMonth = currentMonth, firstDayOfWeek = state.startingDay - ) + ) + } - val calendarState = rememberCalendarState( + val calendarState = remember(state.startingDay) { + rememberCalendarState( startMonth = currentMonth.minusMonths(12), endMonth = currentMonth, firstVisibleMonth = currentMonth, firstDayOfWeek = state.startingDay - ) + ) + }
241-271: Localize content descriptions for iconsHardcoded contentDescription strings (“Warning”, “Delete Habit”, “Edit Habit”, “Add Time”) should use stringResource for i18n and a11y.
Example:
- contentDescription = "Delete Habit" + contentDescription = stringResource(R.string.delete_habit)Also applies to: 421-455
275-285: Persist edit dialog state across config changes (optional)Using rememberSaveable for editDialog/timePickerDialog/new* fields will preserve state on rotation/process death.
- if (editDialog) { - var newHabitTitle by remember { mutableStateOf(currentHabit.title) } + if (editDialog) { + var newHabitTitle by rememberSaveable { mutableStateOf(currentHabit.title) } ... - var timePickerDialog by remember { mutableStateOf(false) } + var timePickerDialog by rememberSaveable { mutableStateOf(false) }Note: add
import androidx.compose.runtime.saveable.rememberSaveable.app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (1)
62-67: Avoid stacking duplicate destinations by using launchSingleTop on navigate.If users tap repeatedly, the typed destinations can stack in the back stack. Use launchSingleTop to keep a single instance.
- onNavigateToOverallAnalytics = { navController.navigate(HabitRoutes.OverallAnalytics) }, - onNavigateToAnalytics = { navController.navigate(HabitRoutes.HabitAnalytics) } + onNavigateToOverallAnalytics = { + navController.navigate(HabitRoutes.OverallAnalytics) { + launchSingleTop = true + } + }, + onNavigateToAnalytics = { + navController.navigate(HabitRoutes.HabitAnalytics) { + launchSingleTop = true + } + }app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (2)
73-76: Prevent duplicate subpages by using launchSingleTop in navigate calls.Navigating from RootPage can push duplicates if tapped repeatedly.
- onNavigateToLookAndFeel = { navController.navigate(SettingsRoutes.LookAndFeel) }, - onNavigateToBackup = { navController.navigate(SettingsRoutes.Backup) }, - onNavigateToAboutLibraries = { navController.navigate(SettingsRoutes.AboutLibraries) }, + onNavigateToLookAndFeel = { + navController.navigate(SettingsRoutes.LookAndFeel) { launchSingleTop = true } + }, + onNavigateToBackup = { + navController.navigate(SettingsRoutes.Backup) { launchSingleTop = true } + }, + onNavigateToAboutLibraries = { + navController.navigate(SettingsRoutes.AboutLibraries) { launchSingleTop = true } + },
103-119: Scope the preview to file only.Mark Preview() as private to avoid unnecessarily exporting a top-level symbol.
-@Preview -@Composable -fun Preview() { +@Preview +@Composable +private fun Preview() {app/src/main/java/com/shub39/grit/app/Grit.kt (1)
75-87: Use graph startDestination for popUpTo to simplify and harden bottom-bar navigation.Hard-coding popUpTo based on a setting can be brittle. Using the graph’s start destination is the canonical pattern and works even if the start page changes later.
- val navigator = { route: Routes -> - if (currentRoute != route) { - navController.navigate(route) { - launchSingleTop = true - popUpTo( - when (settingsState.startingPage) { - Pages.Habits -> Routes.HabitsPages - Pages.Tasks -> Routes.TaskPages - } - ) { saveState = true } - restoreState = true - } - } - } + val navigator = { route: Routes -> + if (currentRoute != route) { + navController.navigate(route) { + launchSingleTop = true + // pop up to the graph's start destination and preserve state + popUpTo(navController.graph.startDestinationId) { saveState = true } + restoreState = true + } + } + }Note: If you prefer typed popUpTo, you can keep the current API but drive the target from a single source of truth, or use findStartDestination().id for equivalent behavior.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
221-223: Stability tweak: derive remember key from category/tasks, not whole state.remember(state) causes unnecessary resets of reorderableTasks on unrelated state changes.
- var reorderableTasks by remember(state) { mutableStateOf(tasks) } + var reorderableTasks by remember(state.currentCategory, tasks) { mutableStateOf(tasks) }
490-494: No-op keyboardActions; either handle IME action or remove.newTitle.plus("\n") and newTask.plus("\n") have no effect. Prefer onDone with ImeAction.Done, or drop keyboardActions.
- keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.None - ), - keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } - ), + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { /* optionally hide keyboard or advance focus */ } + ),- keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.None - ), - keyboardActions = KeyboardActions( - onAny = { - newTask.plus("\n") - } - ), + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { /* optionally hide keyboard or submit */ } + ),Also applies to: 579-582
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
27-28: Avoid symbol shadowing between route object and screen composable by aliasing the importBoth the route (TasksRoutes.EditCategories) and the composable screen (EditCategories) share the same simple name, which can be confusing in reviews and refactors. Aliasing the screen import keeps intent crystal clear.
Apply:
-import com.shub39.grit.tasks.presentation.ui.section.EditCategories +import com.shub39.grit.tasks.presentation.ui.section.EditCategories as EditCategoriesScreenAnd update the call site:
- EditCategories( + EditCategoriesScreen( state = state, onAction = onAction, onNavigateBack = { navController.navigateUp() } )Also applies to: 67-73
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (27)
app/src/main/java/com/shub39/grit/app/Grit.kt(4 hunks)app/src/main/java/com/shub39/grit/app/MainActivity.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/ColorPickerDialog.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/GritBottomSheet.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/GritDialog.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt(3 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt(1 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt(2 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt(2 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt(2 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt(2 hunks)app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt(3 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt(1 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt(1 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt(2 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt(2 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt(2 hunks)app/src/main/java/com/shub39/grit/habits/presentation/util.kt(1 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt(1 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt(3 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/component/TasksGuide.kt(0 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt(1 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt(2 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(4 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/com/shub39/grit/tasks/presentation/component/TasksGuide.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/app/Grit.kt (3)
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
TasksGraph(40-75)app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (1)
SettingsGraph(43-101)app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (1)
HabitsGraph(46-86)
🔇 Additional comments (25)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
1-1: Package move looks good.Matches the components→component reorg across the codebase.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
1-1: Package move LGTM.Matches the ui.component reorg; imports will resolve accordingly.
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)
1-1: Package move LGTM.Consistent with ui.component reorg; dependent imports in ui.section appear updated.
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt (1)
1-1: Package move LGTM.Aligns with components→component consolidation.
app/src/main/java/com/shub39/grit/app/MainActivity.kt (1)
118-126: Confirm UX on biometric cancellation (finishing the Activity).
On user cancel or negative button, you toast and then call finish(). If the intended UX is to keep the app open but locked, consider returning to the lock screen instead of exiting.app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
1-1: Package rename looks correct and consistent with the reorg.
No functional changes; call sites should continue to work with the new path.app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt (1)
1-1: Namespace/import migration looks consistent with the project reorg.The package move and PageFill import update align with the plural→singular component consolidation. No functional changes introduced here.
Also applies to: 37-37
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt (1)
1-1: Package/import updates look good.Matches the broader move to settings.ui.section and core.presentation.component.
Also applies to: 22-22
app/src/main/java/com/shub39/grit/core/presentation/component/ColorPickerDialog.kt (1)
1-1: Package move aligns with core component consolidation.No functional changes; dialog composition and callbacks look correct.
app/src/main/java/com/shub39/grit/core/presentation/component/GritBottomSheet.kt (1)
1-1: Namespace update looks good; API remains stable.Sheet configuration (max width, skip partial) and Column contract are sensible defaults.
app/src/main/java/com/shub39/grit/core/presentation/component/GritDialog.kt (1)
1-1: Package rename to singular namespace looks goodThis aligns with the broader reorg (components → component). No functional changes; API remains stable.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (3)
1-1: Package move to ui.section is consistent with the reorganisationMatches the new module layout; no API/behavior change introduced here.
52-55: Updated imports to singular component packageGritBottomSheet/GritDialog/PageFill import paths are correct post-move.
239-245: Ignore the UpdateCategory suggestion – AddCategory is already an upsertThe
TaskPageAction.AddCategoryaction callsrepo.upsertCategory, and becausecategory.copy(name = name)preserves the existingid, it updates rather than duplicates. There is no existingUpdateCategoryorRenameCategory, and introducing one isn’t necessary for correctness.• In TaskPageAction.kt (lines 7–8),
data class AddCategory(val category: Category)is the sole category‐upsert action.
• In TasksViewModel.kt (lines 59–63),is TaskPageAction.AddCategoryinvokesupsertCategory(action.category), which updates by id.
• If you find the name confusing, you could refactorAddCategory→UpsertCategoryin a follow-up change, but it isn’t required now.Likely an incorrect or invalid review comment.
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)
1-1: Package path updated to settings.ui.component is consistentMatches the settings reorg; usage in RootPage is updated accordingly.
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (1)
1-1: Namespace/import updates are correctPageFill import moved to singular; AboutApp is correctly sourced from settings.ui.component.
Also applies to: 37-38, 40-41
app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (2)
22-29: Rename to HabitsGraph and component/ui.section import migration look consistent.The API rename + typed navigation setup align with the PR’s re-organization and other graphs. No functional concerns here.
Also applies to: 47-50
139-145: No action required:HabitStatus.idhas a default value
TheHabitStatusdata class declaresid: Long = 0, so callingHabitStatus(habitId = habit.id, date = it.date)without supplying anidis valid and will default to0. No changes are needed here.app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (1)
20-27: Settings → SettingsGraph migration and component/ui.section imports look good.Typed routes and PageFill usage are consistent with the rest of the app graphs.
Also applies to: 44-47, 114-117
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt (1)
1-1: Package + component import reorg looks correct.Also applies to: 77-80
app/src/main/java/com/shub39/grit/app/Grit.kt (1)
36-38: Graph composable adoption (TasksGraph/SettingsGraph/HabitsGraph) is consistent and correct.Also applies to: 157-161, 166-169, 178-181
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
70-73: Import reorg to component/ui and TaskCard new location looks good.Also applies to: 79-79
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (3)
117-129: LGTM: Preview updated to use TasksGraph after renameThe preview references the renamed composable and compiles the happy path interaction. Looks good.
22-22: ✅ Migration Complete — No Remaining Old Imports FoundI reran a repository-wide search for any
import com.shub39.grit.core.presentation.components.PageFillreferences across all Kotlin files (excluding build artifacts) and found zero matches. The singularcomponent.PageFillimport is now consistently used throughout the codebase. No further action is needed.
41-45: Rename completed: no staleTasksreferences foundVerified via project-wide search that there are
- no remaining imports of
com.shub39.grit.tasks.presentation.Tasks- no unqualified calls to
Tasks(...)- all call sites now reference
TasksGraph(...)(in TasksGraph.kt and Grit.kt)The rename is fully applied—no further changes needed here.
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import com.shub39.grit.R | ||
| import com.shub39.grit.core.presentation.components.InitialLoading | ||
| import com.shub39.grit.core.presentation.component.InitialLoading |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Import path update looks good; run a quick sweep for stale package paths.
The switch to core.presentation.component.InitialLoading aligns with the reorg.
Run this to catch any lingering old imports or misspellings:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Old plural namespace
rg -nP '\bcom\.shub39\.grit\.core\.presentation\.components\b' -g 'app/src/**' || true
# Old habits UI path (moved to ui.component)
rg -nP '\bcom\.shub39\.grit\.habits\.presentation\.component\b' -g 'app/src/**' || true
# Old tasks UI path (moved to ui.component)
rg -nP '\bcom\.shub39\.grit\.tasks\.presentation\.component\b' -g 'app/src/**' || true
# Ensure InitialLoading composable exists, and no leftover misspelling
rg -nP 'fun\s+InitialLoading\s*\(' -g 'app/src/**' || true
rg -nP 'InitialLoding' -g 'app/src/**' || trueLength of output: 488
Rename misspelled file for InitialLoading composable
All legacy import paths have been swept and none remain, but there’s a typo in the filename:
- app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt
→ Rename to InitialLoading.kt to match theInitialLoading()composable and avoid confusion.
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/app/MainActivity.kt around line 18, the
import references com.shub39.grit.core.presentation.component.InitialLoading but
the actual source file is misspelled as InitialLoding.kt; rename the file
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt
to InitialLoading.kt (update filesystem and VCS), and ensure the Kotlin file’s
class/composable name and any package declaration remain unchanged so the import
compiles correctly.
| import sh.calvin.reorderable.rememberReorderableLazyListState | ||
| import java.time.DayOfWeek |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing import for draggableHandle causes a compile error.
Modifier.draggableHandle is an extension; without its import, this file won’t compile.
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import sh.calvin.reorderable.rememberReorderableLazyListState | |
| import java.time.DayOfWeek | |
| import sh.calvin.reorderable.ReorderableItem | |
| import sh.calvin.reorderable.rememberReorderableLazyListState | |
| import sh.calvin.reorderable.draggableHandle |
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt
around lines 65-66, the file is missing the import for the
Modifier.draggableHandle extension which causes a compile error; add the import
line for the extension (import sh.calvin.reorderable.draggableHandle) alongside
the other imports at the top of the file so the draggableHandle modifier
resolves.
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.graphics.Color |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Preview Theme: seedColor usage looks fine — confirm new Theme API is consistently adopted elsewhere
Setting isMaterialYou = false, seedColor = Color.Red, and paletteStyle = TonalSpot is fine for a deterministic preview. Two quick checks:
- Ensure all
Theme(...)constructor calls across the app are updated to pass the newseedColor(or rely on an intended default). - Confirm the Theme implementation actually uses
seedColorwhenisMaterialYou = falseso the preview reflects the intended palette.
Minor nit: if preview builds feel heavy, consider using a subtler brand color (or centralize a PreviewThemeDefaults.seedColor) to keep previews consistent.
Also applies to: 105-111
🏁 Script executed:
#!/bin/bash
# Where is Theme(...) constructed?
rg -nP --type=kt -g '!**/build/**' '\bTheme\s*\(' -C2
# Do those call sites include an explicit seedColor?
rg -nP --type=kt -g '!**/build/**' 'seedColor\s*=' -nLength of output: 165
🏁 Script executed:
#!/bin/bash
# Find all calls to Theme(...) in Kotlin files outside the build directory, with context
rg -n '\bTheme\s*\(' -g '*.kt' -g '!**/build/**' -C2
# Find all explicit seedColor assignments in Kotlin files outside the build directory
rg -n 'seedColor\s*=' -g '*.kt' -g '!**/build/**'Length of output: 4161
🏁 Script executed:
#!/bin/bash
# Display the contents of GritTheme.kt to verify seedColor usage logic
sed -n '1,200p' app/src/main/java/com/shub39/grit/core/presentation/theme/GritTheme.ktLength of output: 1213
🏁 Script executed:
#!/bin/bash
# Print the contents of Theme.kt to inspect seedColor and paletteStyle parameters
sed -n '1,200p' app/src/main/java/com/shub39/grit/core/presentation/theme/Theme.ktLength of output: 575
Preview Theme API adoption verified—update HabitsGraph preview for consistency
I’ve confirmed that the new Theme API is wired up correctly:
- GritTheme.kt uses
theme.seedColorwheneverisMaterialYou = false, and only falls back to the system accent whenisMaterialYou = trueand on Android 12+. - All existing preview composers now explicitly pass
seedColorand/or setisMaterialYou = false—with one exception below.
Please adjust the HabitsGraph preview so it too renders deterministically:
• File: app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (around lines 113–117)
Change from:
GritTheme(
theme = Theme(
appTheme = AppTheme.DARK,
isMaterialYou = true
)
) { … }To something like:
GritTheme(
theme = Theme(
appTheme = AppTheme.DARK,
isMaterialYou = false,
seedColor = Color.Green, // or your chosen preview brand color
paletteStyle = ToneSpot
)
) { … }Minor nit: consider centralizing these preview defaults (e.g. PreviewThemeDefaults.seedColor) if you find yourself reusing the same colors.
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt around
lines 113–117, the preview GritTheme still sets isMaterialYou = true and thus
renders non-deterministically; update the Theme passed to GritTheme to set
isMaterialYou = false, supply a fixed seedColor (e.g. Color.Green or your chosen
preview brand color) and set paletteStyle = ToneSpot so the preview renders
deterministically; optionally extract these preview defaults into a shared
constant (e.g., PreviewThemeDefaults.seedColor) if reused elsewhere.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
gradle/libs.versions.toml (2)
21-21: Jetpack Compose UI = "1.9.0" — verify this version exists and align with Navigation & Material3; consider migrating to the Compose BOM.Hard-pinning individual Compose artifacts often creates drift. The BOM ensures consistent, supported coordinates across ui, material3, tooling, etc.
Option A — adopt Compose BOM in the version catalog and drop per-artifact versions:
Add a BOM alias (version managed once):
[toml]
[versions]
composeBom = "REPLACE_WITH_PUBLISHED_BOM"[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }Remove version.ref from androidx-ui, ui-graphics, ui-tooling, ui-tooling-preview, material3, and navigation-compose, and import the BOM in Gradle:
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
debugImplementation(libs.androidx.ui.tooling)
}If you prefer to keep explicit versions, please confirm that androidx.compose.ui:ui:1.9.0 is published and compatible with your Material3 and Navigation versions using the script in the next comment. I can generate an exact BOM version once you confirm the target Compose train.
13-13: Alpha/stable mix: Material3 "1.5.0-alpha02" + Navigation "2.9.3" — double-check train compatibility.Mixing alpha M3 with different generation Navigation and a possibly out-of-train Compose UI can cause API/ABI conflicts or missing symbols at runtime.
- Prefer keeping Material3, Compose UI, and Navigation on the same release train (via BOM).
- If you need an alpha M3 feature (e.g., Theme.seedColor previews), fix the train via BOM and only override the specific artifact if necessary, keeping others within the same snapshot line.
Use the dependency verification script below (next comment) to confirm these versions exist and co-resolve cleanly.
Also applies to: 16-16
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
gradle/libs.versions.toml(1 hunks)
🔇 Additional comments (3)
gradle/libs.versions.toml (3)
3-5: Third-party dependencies verified and publishedAll of the updated coordinates have been confirmed in their respective Maven repositories—no missing versions detected. Feel free to proceed with merging.
11-12: Kotlin & KSP Version Verification Complete
- Verified on Maven Central:
- org.jetbrains.kotlin:kotlin-gradle-plugin 2.2.10 ✅
- com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin 2.2.10-2.0.2 ✅
- com.google.devtools.ksp:symbol-processing-api 2.2.10-2.0.2 ✅
These artifacts are published and the Kotlin 2.2.10 / KSP 2.2.10-2.0.2 pairing is valid. No further changes are required here.
2-2: AGP 8.12.1 Verified and CompatibleYour script confirms that
com.android.tools.build:gradle:8.12.1is published on Google Maven and your Gradle wrapper is set to version 8.13. According to the official Android Gradle plugin release notes, AGP 8.12.x requires Gradle 8.13 as the minimum supported version (developer.android.com). You can safely retainagp = "8.12.1"in
gradle/libs.versions.toml.
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
221-228: Two fixes: initialize by category/tasks and reorder against the working list.
remember(state)risks holding stale data ifstateis referentially stable; key oncategory/tasks.- Reorder callback must mutate from the current working order, not the original
taskssnapshot, or indices can drift after the first move.Apply this diff:
- var reorderableTasks by remember(state) { mutableStateOf(tasks) } + var reorderableTasks by remember(category, tasks) { mutableStateOf(tasks) } @@ - rememberReorderableLazyListState(lazyListState) { from, to -> - reorderableTasks = tasks.toMutableList().apply { - add(to.index, removeAt(from.index)) - } - } + rememberReorderableLazyListState(lazyListState) { from, to -> + reorderableTasks = reorderableTasks.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }
594-600: Bug: index calculated for the wrong category when adding a task.If the user selects a different category in the sheet, the new task’s
indexshould be based onnewTaskCategoryId, notstate.currentCategory. This can cause corrupted ordering in the target category.Apply this diff:
onAction( TaskPageAction.UpsertTask( Task( categoryId = newTaskCategoryId, title = newTask, status = false, - index = state.tasks[state.currentCategory]?.size ?: 0 + index = state.tasks.entries + .firstOrNull { it.key.id == newTaskCategoryId } + ?.value?.size ?: 0 ) ) )app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (1)
6-7: Fix missing Material3 Expressive dependency or remove its usageIt looks like you’re importing ContainedLoadingIndicator and opting into ExperimentalMaterial3ExpressiveApi, but there’s no material3-expressive artifact declared in your version catalog or Gradle scripts. As a result, the imports won’t resolve and the code will fail to compile.
• File app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt – lines 6–7:
-import androidx.compose.material3.ContainedLoadingIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.CircularProgressIndicator @@ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ - ContainedLoadingIndicator() + CircularProgressIndicator()If you truly need the expressive loading indicator, add the expressive artifact to your version catalog or Gradle dependencies (e.g.
androidx.compose.material3:material3-expressive:<version>), otherwise switch to CircularProgressIndicator to avoid the experimental opt-in and missing-dependency issue.
♻️ Duplicate comments (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
270-278: Move completed task using the working list, not the original snapshot.This recreates the list from
tasks, which can discard in-flight reorders and misplace items. Operate onreorderableTasksinstead. Also, prefer using the target list’ssizewhen appending to the end to avoid off-by-one coupling to the old list size.Apply this diff:
- reorderableTasks = - tasks.toMutableList().apply { - add(reorderableTasks.size - 1, removeAt(index)) - } + reorderableTasks = + reorderableTasks.toMutableList().apply { + // remove from current position and append to end + add(size, removeAt(index)) + }
🧹 Nitpick comments (6)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
166-175: Avoid persisting the original snapshot on category switch.
onClicksendsReorderTasks(tasks.mapIndexed { ... }), which persists the original snapshot, not the user’s working order. Either:
- Remove this call and rely on drag-stop/complete-tick flows to persist, or
- Hoist
reorderableTasksso it’s available here (or move the chips into the same scope) and persist the working list instead.If you keep the flush here, ensure it uses the current working list. Otherwise, consider removing it to avoid persisting stale ordering.
490-494: No-op KeyboardActions handlers.
newTitle.plus("\n")/newTask.plus("\n")returns a new string and doesn’t change state. Either assign the result or remove the handler to avoid confusion. Given both text fields are single-line or haveImeAction.None, removing is fine.Example cleanup:
- keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } - ), + // No keyboardActions: single-line editingAlso applies to: 579-581
191-195: Localize content descriptions.Hardcoded English strings (“Add Category”, “Edit Categories”, “Drag”, “Warning”, “Add”, “Edit”) should use
stringResource(...)for i18n consistency with the rest of the screen.Example:
- contentDescription = "Add Category" + contentDescription = stringResource(R.string.add_category)Apply similarly to the other icons above.
Also applies to: 207-211, 245-247, 344-347, 389-391, 458-460, 546-548
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (3)
15-19: Expose a Modifier parameter for composability and reuse.Standard Compose convention: accept
modifier: Modifier = Modifierto let callers control size/placement without forking this composable.Apply:
-@Composable -fun InitialLoading() { +@Composable +fun InitialLoading(modifier: Modifier = Modifier) { Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center ) {
22-22: Add basic a11y semantics so TalkBack announces progress.Annotate the indicator with a polite live region and a descriptive label; this improves accessibility on the initial loading screen.
Apply:
- ContainedLoadingIndicator() + ContainedLoadingIndicator( + modifier = Modifier.semantics { + contentDescription = "Loading" + liveRegion = LiveRegionMode.Polite + } + )Add imports (outside the selected range):
import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semanticsOptionally, source the label from strings for localization and pass it as a parameter.
18-19: Minor theming nit: prefer surface over background for screen containers.Using
colorScheme.surface(or a Surface composable) better aligns with Material 3 tonal elevation. Not required if you intentionally want the raw background color.Apply:
- .background(MaterialTheme.colorScheme.background), + .background(MaterialTheme.colorScheme.surface),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt(1 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(4 hunks)
🔇 Additional comments (4)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
70-73: Imports migration to core.presentation.component looks good.The move from components → component aligns with the PR’s reorg and keeps call sites consistent.
78-78: TaskCard import from ui.component is consistent with new structure.This keeps rendering concerns localized under ui.component. LGTM.
249-252: Correct: persist working order on drag stop.Flushing
reorderableTasks.mapIndexed { index to task }on drag end is the right moment to persist the visible order.app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (1)
1-1: No residual “components” references found—singular “component” package confirmed
- Executed ripgrep scans across the codebase for any imports or package declarations of
com.shub39.grit.core.presentation.componentsand found zero matches.- All source files now correctly declare and import from
com.shub39.grit.core.presentation.component.The rename is consistent and safe to merge.
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
223-227: Reorder callback mutates a stale snapshot (tasks) instead of the working list (reorderableTasks)This will clobber any in-progress reorders and can misplace items. The move operation should start from
reorderableTasks.Apply one of the following diffs:
Minimal fix (keep current add/remove semantics):
- rememberReorderableLazyListState(lazyListState) { from, to -> - reorderableTasks = tasks.toMutableList().apply { - add(to.index, removeAt(from.index)) - } - } + rememberReorderableLazyListState(lazyListState) { from, to -> + val mutable = reorderableTasks.toMutableList() + val moved = mutable.removeAt(from.index) + // If moving down, adjust target index after removal. + val target = if (to.index > from.index) to.index - 1 else to.index + mutable.add(target, moved) + reorderableTasks = mutable + }Or, if you have/accept a small helper (safer and clearer):
+fun <T> MutableList<T>.move(from: Int, to: Int) { + if (from == to) return + val item = removeAt(from) + add(if (to > from) to - 1 else to, item) +} - rememberReorderableLazyListState(lazyListState) { from, to -> - reorderableTasks = tasks.toMutableList().apply { - add(to.index, removeAt(from.index)) - } - } + rememberReorderableLazyListState(lazyListState) { from, to -> + reorderableTasks = reorderableTasks.toMutableList().apply { move(from.index, to.index) } + }
268-281: Reinsertion on status toggle usestasksand mismatched indicesWhen marking a task completed, the code rebuilds from
tasks.toMutableList()and then removes atindexfrom the currentreorderableTaskscontext. That index may not correspond to the same element intasks, leading to incorrect removals and lost reorders.Apply this diff:
- if (task.status) { - reorderableTasks = - tasks.toMutableList().apply { - add( - reorderableTasks.size - 1, - removeAt(index) - ) - } + if (task.status) { + reorderableTasks = reorderableTasks.toMutableList().apply { + add(size - 1, removeAt(index)) + } onAction( TaskPageAction.ReorderTasks( reorderableTasks.mapIndexed { index, task -> index to task }) ) }
🧹 Nitpick comments (5)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
12-14: Nit: fix spacing and conventional type syntax in the signatureMinor polish for consistency with Kotlin/Compose style.
Apply this diff:
-@Composable -fun PageFill( - content: @Composable (BoxScope.() -> Unit) -) { +@Composable +fun PageFill( + content: @Composable BoxScope.() -> Unit, +) {app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
220-224: Tighten the remember keys to avoid unnecessary resets ofreorderableTasks
remember(state)will reset the working list on any state change (even unrelated to the current list), which can be jarring mid-edit. Scope it to the actual inputs.Apply this diff:
- var reorderableTasks by remember(state) { mutableStateOf(tasks) } + var reorderableTasks by remember(state.currentCategory, tasks) { mutableStateOf(tasks) }If you want to persist local reorders until an explicit commit, consider:
var reorderableTasks by remember { mutableStateOf(tasks) } LaunchedEffect(tasks) { reorderableTasks = tasks }
295-301: Prefer explicit size over padding-only Spacer
Spacer(modifier = Modifier.padding(60.dp))can be confusing; a height-based spacer communicates intent.Apply this diff:
- } else { - Spacer(modifier = Modifier.padding(60.dp)) - } + } else { + Spacer(modifier = Modifier.size(60.dp)) + }If you only need vertical spacing, use
height(60.dp).
491-495: No-op KeyboardActions: remove or actually update the state
KeyboardActions(onAny = { newTitle.plus("\n") })andnewTask.plus("\n")do nothing because the result isn’t assigned. Also, withImeAction.None, this likely never fires.Apply this diff to remove the dead code:
- keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } - ), + // No keyboardActions needed; multiline input handled by default.and
- keyboardActions = KeyboardActions( - onAny = { - newTask.plus("\n") - } - ), + // No keyboardActions needed; multiline input handled by default.If your intent was to insert a newline on IME action, set an IME action and update the state:
onDone = { newTitle += "\n" }.Also applies to: 579-583
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
55-64: Consider consistent layering: wrap the Tasks graph with PageFill like Habits/SettingsHabitsGraph/SettingsGraph use
PageFill { ... }at the graph level, while TasksGraph delegatesPageFilltoTaskList. Not a bug, but aligning the pattern keeps app scaffolding consistent and leaves sections free to focus on content.Example:
-@Composable -fun TasksGraph( +@Composable +fun TasksGraph( state: TaskPageState, onAction: (TaskPageAction) -> Unit ) { - val navController = rememberNavController() + val navController = rememberNavController() + PageFill { /* NavHost ... */ + } }If you do this, remove the
PageFillwrapper fromTaskListto avoid nesting Boxes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
app/build.gradle.kts(1 hunks)app/src/main/java/com/shub39/grit/app/Grit.kt(7 hunks)app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt(2 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt(3 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(6 hunks)gradle/libs.versions.toml(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- app/build.gradle.kts
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/shub39/grit/app/Grit.kt (3)
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
TasksGraph(39-74)app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (1)
SettingsGraph(43-101)app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (1)
HabitsGraph(46-86)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
TaskCard(27-88)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
🔇 Additional comments (11)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
13-19: Good API upgrade: BoxScope receiver enables alignment and intrinsics within contentChanging
contentto@Composable BoxScope.() -> Unitis the right move. It unlocksModifier.align(...)and other BoxScope APIs for callers (e.g., that FAB aligned to BottomEnd in TaskList). Usingcontent = contentwithcontentAlignmentis also clean.app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
99-104: TopAppBar scroll behavior integration looks correctUsing
TopAppBarDefaults.enterAlwaysScrollBehavior()and wiring it viaModifier.nestedScroll(scrollBehaviour.nestedScrollConnection)on the scrolling container is the right pattern for a collapsing bar.
307-337: FAB overlay via BoxScope alignment works wellPlacing the FAB with
Modifier.align(Alignment.BottomEnd)insidePageFill’s BoxScope is clean and keeps it above the scrolling content. Good use ofAnimatedVisibilityto gate it bycurrentCategory.app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (2)
40-44: Rename to TasksGraph and route structure look goodThe sealed routes + NavHost setup is tidy. Swapping in
TaskListand navigating toEditCategoriesthrough typed routes is straightforward.
104-110: Preview: deterministic theme configuration is appreciatedUsing
isMaterialYou = false,seedColor = Color.Red, andTonalSpotproduces stable previews. Matches recent theming guidance across the project.app/src/main/java/com/shub39/grit/app/Grit.kt (4)
138-143: RTL-aware padding: nice upgradeSwitching to
calculateStartPadding/calculateEndPaddingensures proper insets in RTL locales. Good attention to detail.
97-129: Bottom navigation simplification looks solidUsing
NavigationBarwithout the previous rounded-clip keeps the layout simpler. Icons/labels are set correctly withalwaysShowLabel = false.
154-167: Graph adoption wired correctly
TasksGraph,SettingsGraph, andHabitsGraphare all invoked with the correct states and actions after the renames.
36-39: All old composable references have been removedI performed a comprehensive code-wide search for any calls or imports of the legacy
Tasks,Settings, andHabitscomposables and found no matches—there are no lingering references to the old names. This item can be closed.gradle/libs.versions.toml (2)
2-5: LGTM: Dependency version updates look reasonable.The version bumps appear to be incremental updates that should maintain compatibility. The changes include:
- AGP 8.11.1→8.12.1 (patch update)
- Kotlin 2.2.0→2.2.10 (patch update)
- Material3 1.4.0-alpha17→1.5.0-alpha02 (minor alpha update)
- Compose 1.8.3→1.9.0 (minor update)
- Other incremental updates to calendar, composeCharts, purchases, etc.
Also applies to: 11-14, 16-16, 18-18, 21-22
14-14: Reorderable bumped to 3.0.0 – LazyList API unchangedI’ve reviewed the v3.0.0 release notes and confirmed that the only breaking changes concern the non-lazy containers (
ReorderableColumn/ReorderableRow) and grid auto-scroll behavior—neither of which is used in our codebase (github.com). The official v3.0.0 README still shows the exact samerememberReorderableLazyListState+ReorderableItempattern we use in bothTaskList.ktandHabitsList.kt(github.com). No migration changes are needed for our LazyColumn-based reorderable lists.
| compose = "1.8.3" | ||
| materialIconsCore = "1.7.3" | ||
| compose = "1.9.0" | ||
| materialIconsExtended = "1.7.8" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify compatibility of the new material icons extended library.
The addition of materialIconsExtended = "1.7.8" and the corresponding library androidx-material-icons-extended suggests a migration from a previous icons library. The AI summary mentions that material-icons-core was removed.
Also applies to: 27-27
🏁 Script executed:
#!/bin/bash
# Description: Verify that the new material icons extended library is properly used and replaces the old one
# Expected: Find usages of the new library and ensure no references to the old material-icons-core remain
echo "=== Searching for usage of new material icons extended library ==="
rg -n "androidx.material.icons.extended" --type=gradle
echo "=== Searching for any remaining references to old material-icons-core ==="
rg -n "material.icons.core" --type=gradle
echo "=== Checking imports in Kotlin files for material icons ==="
rg -n "import.*material.*icons" --type=kotlin -C2
echo "=== Verifying the library is actually used in build.gradle files ==="
rg -n "libs\.androidx\.material\.icons\.extended" --type=gradleLength of output: 10406
🏁 Script executed:
#!/bin/bash
# Display content around the materialIconsExtended version and library aliases
FILE="gradle/libs.versions.toml"
if [ -f "$FILE" ]; then
echo "=== First 100 lines of $FILE ==="
sed -n '1,100p' "$FILE"
echo
echo "=== Searching for 'extended' in $FILE ==="
rg -n "extended" "$FILE"
echo
echo "=== Searching for 'icons' in $FILE ==="
rg -n "icons" "$FILE"
else
echo "Error: $FILE not found"
fi
echo
# Search for alias usage in the codebase
echo "=== Searching for 'materialIconsExtended' references in code ==="
rg -n "materialIconsExtended" -C2 .
echo
# Search for dependency inclusion in Gradle build files
echo "=== Searching for 'material-icons-extended' in build scripts ==="
rg -n "material-icons-extended" -C2 --glob="*.gradle*" .Length of output: 5965
🏁 Script executed:
#!/bin/bash
echo "=== Searching for core alias in build scripts ==="
rg -n "libs\.androidx\.material\.icons\.core" -C2 --glob="*.gradle*"
echo
echo "=== Searching for literal core artifact usage ==="
rg -n "androidx\.compose\.material:material-icons-core" -C2 --glob="*.gradle*"
echo
echo "=== Searching for 'material-icons-core' in all files ==="
rg -n "material-icons-core" -C2 .Length of output: 548
Action Required: Material Icons Extended alias defined but not applied
The materialIconsExtended = "1.7.8" version and its androidx-material-icons-extended alias are correctly declared in gradle/libs.versions.toml (lines 22–27), but no module-level build files reference this new library. As a result, the extended icon set isn’t actually added to the classpath. To complete the migration:
• gradle/libs.versions.toml (lines 22–27)
- Alias is present but unused.
• All module-level build.gradle(.kts) files
- Under
dependencies { … }, add:dependencies { + implementation(libs.androidx.material.icons.extended) // other dependencies… }
• Kotlin UI files importing icons
- If you intend to use icons provided only by the extended library, update imports from the core namespace to the extended one. For example:
- import androidx.compose.material.icons.filled.Create + import androidx.compose.material.icons.extended.filled.Create
• Cleanup old core dependency (if previously declared explicitly)
- No explicit
material-icons-coredependency was found, but verify that you’re not still relying on the legacy core artifact.
Please add the dependency in each module that needs extended icons and update imports accordingly to ensure the new library is actually used.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In gradle/libs.versions.toml around lines 22 to 22, the alias
materialIconsExtended = "1.7.8" is declared but not used; to fix, add the
extended icons dependency to every module that uses Material icons by adding the
corresponding entry under that module's dependencies block referencing the new
alias (e.g., implement/libraries using the libs alias for
androidx-material-icons-extended), update Kotlin Compose imports in UI files
that need extended icons from the core namespace (e.g.,
androidx.compose.material.icons.*) to the extended namespace (e.g.,
androidx.compose.material.icons.extended.*), and remove or stop relying on any
explicit legacy/core material-icons dependency if present so the extended
artifact is actually used.
* updated icons to imagevectors * used m3 expressive components Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
252-265: Moving a completed task must operate on reorderableTasks, not tasks, and should append to the end clearly.You’re rebuilding from tasks (original snapshot) which can drop prior reorders; this also mirrors a previously reported issue. Prefer appending via add(element) for clarity.
Apply this diff:
- if (task.status) { - reorderableTasks = - tasks.toMutableList().apply { - add( - reorderableTasks.size - 1, - removeAt(index) - ) - } + if (updated.status) { + reorderableTasks = + reorderableTasks.toMutableList().apply { + // move the toggled item to the end + add(removeAt(index)) + } onAction( TaskPageAction.ReorderTasks( reorderableTasks.mapIndexed { index, task -> index to task }) ) }Additionally, to avoid in-place mutation on a list element (better state semantics in Compose), update the toggle to use an immutable copy:
- if (!editState) { - task.status = !task.status - - onAction(TaskPageAction.UpsertTask(task)) + if (!editState) { + val updated = task.copy(status = !task.status) + onAction(TaskPageAction.UpsertTask(updated))Note: This also requires the guard to check updated.status as shown above.
206-211: Reordering uses the immutable snapshot (tasks) instead of the working list (reorderableTasks).This resets any in-progress order and can misplace items during drag. Use reorderableTasks as the source of truth inside the reorder callback.
Apply this diff:
- val reorderableListState = - rememberReorderableLazyListState(lazyListState) { from, to -> - reorderableTasks = tasks.toMutableList().apply { - add(to.index, removeAt(from.index)) - } - } + val reorderableListState = + rememberReorderableLazyListState(lazyListState) { from, to -> + reorderableTasks = reorderableTasks.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + }
🧹 Nitpick comments (8)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (8)
168-171: Category change commits the stale order (tasks) instead of the working order (reorderableTasks).This can drop uncommitted reorders if the user switches categories mid-reorder. Either:
- Commit from reorderableTasks, or
- Omit the commit here and rely on the onDragStopped commit.
Given reorderableTasks is scoped inside AnimatedContent, the minimal safe fix is to remove the commit here.
Apply this diff to remove the stale commit:
- onAction(TaskPageAction.ReorderTasks(tasks.mapIndexed { index, task -> index to task })) onAction(TaskPageAction.ChangeCategory(category)) editState = falseIf you prefer to keep the commit on navigation, hoist reorderableTasks to a scope accessible here (e.g., remember keyed by category outside AnimatedContent) and commit from that instead.
246-251: Avoid in-place mutation of task.status; prefer immutable updates.Directly mutating an element inside a remembered list can lead to subtle recomposition issues. Use copy and dispatch the new instance.
Apply this diff (coordinates with the later move logic fix):
- if (!editState) { - task.status = !task.status - - onAction(TaskPageAction.UpsertTask(task)) + if (!editState) { + val updated = task.copy(status = !task.status) + onAction(TaskPageAction.UpsertTask(updated))And replace subsequent references to task.status in this onClick block with updated.status.
100-111: Minor naming consistency: use scrollBehavior (US spelling) to match the API and reduce cognitive friction.Purely stylistic, but aligns with TopAppBarDefaults.scrollBehavior property naming across Compose.
Apply this diff:
- val scrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior() + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() @@ - modifier = Modifier - .nestedScroll(scrollBehaviour.nestedScrollConnection) + modifier = Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) @@ - scrollBehavior = scrollBehaviour, + scrollBehavior = scrollBehavior,
204-206: Reinitializing reorderableTasks on any state change may be too aggressive.remember(state) will reset the working list even for unrelated state changes (e.g., counters), potentially interrupting the UX. Consider scoping to category and/or the tasks list identity.
Apply this diff to reduce churn while still reacting to real task-list changes:
- var reorderableTasks by remember(state) { mutableStateOf(tasks) } + var reorderableTasks by remember(category, tasks) { mutableStateOf(tasks) }If the tasks list identity is stable on content mutations (new list instances on any change), the above will keep reorderableTasks in sync without resetting on unrelated state updates.
146-148: Enable condition can use the local tasks to avoid repeating a nullable map lookup.Functionally equivalent but simpler and null-safe by construction.
Apply this diff:
- enabled = !state.tasks[state.currentCategory].isNullOrEmpty() + enabled = tasks.isNotEmpty()
183-195: Replace hard-coded contentDescription strings with localized stringResources (and mark purely decorative icons as null).This improves accessibility and i18n. For icons that convey no extra information beyond adjacent text, set contentDescription = null; otherwise use stringResource.
Apply these diffs:
- Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Add Category" - ) + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.add_category) + )- Icon( - imageVector = Icons.Rounded.Edit, - contentDescription = "Edit Categories" - ) + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = stringResource(R.string.edit_categories) + )- Icon( - imageVector = Icons.Rounded.DragIndicator, - contentDescription = "Drag", + Icon( + imageVector = Icons.Rounded.DragIndicator, + contentDescription = stringResource(R.string.drag), modifier = Modifier.draggableHandle(- Icon( - imageVector = Icons.Rounded.Warning, - contentDescription = "Warning", + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringResource(R.string.warning),- Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Add" + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.add)- Icon( - imageVector = Icons.Rounded.Edit, - contentDescription = "Edit" + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = stringResource(R.string.edit)If some of these string resources don’t exist yet (e.g., edit_categories, drag, warning), I can add them to strings.xml. Want me to open a follow-up PR?
Also applies to: 228-231, 330-333, 374-376, 443-446, 531-533
475-479: No-op keyboard action.newTitle.plus("\n") doesn’t mutate state; the call result is discarded. Either remove keyboardActions or actually update state.
Apply one of:
Option A — remove it:
- keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } - ), + // no keyboardActions needed; multi-line input already supports newlineOption B — mutate:
keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } + onAny = { newTitle += "\n" } ),
563-567: Same no-op keyboard action in Add Task sheet.The .plus call does not change newTask.
Apply one of:
Option A — remove:
- keyboardActions = KeyboardActions( - onAny = { - newTask.plus("\n") - } - ), + // no keyboardActions needed; multi-line input already supports newlineOption B — mutate:
keyboardActions = KeyboardActions( - onAny = { - newTask.plus("\n") - } + onAny = { newTask += "\n" } ),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
TaskCard(27-88)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
Show resolved
Hide resolved
* stashing changes Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)
161-170: Empty onClick handler for drag handle button.The FilledTonalIconButton has an empty onClick handler, which doesn't follow Material Design guidelines. Since this is a drag handle that should only respond to drag gestures, consider using a non-interactive visual element instead.
- FilledTonalIconButton( - onClick = {}, - modifier = Modifier.draggableHandle() - ) { - Icon( - imageVector = Icons.Rounded.DragIndicator, - contentDescription = null - ) - } + Icon( + imageVector = Icons.Rounded.DragIndicator, + contentDescription = "Drag to reorder", + modifier = Modifier.draggableHandle() + )app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
475-478: KeyboardActions.onAny implementation has no effect.The
onAnykeyboard action attempts to append a newline but doesn't actually modify the state. The expressionnewTitle.plus("\n")creates a new string without assigning it anywhere.keyboardActions = KeyboardActions( onAny = { - newTitle.plus("\n") + newTitle = newTitle.plus("\n") } ),Alternatively, if multiline input isn't needed, consider removing this keyboard action entirely:
- keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } - ),
562-566: KeyboardActions.onAny implementation has no effect.Same issue as above - the keyboard action doesn't actually modify the state.
keyboardActions = KeyboardActions( onAny = { - newTask.plus("\n") + newTask = newTask.plus("\n") } ),Or remove if multiline isn't needed:
- keyboardActions = KeyboardActions( - onAny = { - newTask.plus("\n") - } - ),
♻️ Duplicate comments (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
207-210: Reordering uses incorrect source list after initial drag.The reordering operation uses
tasksinstead ofreorderableTasksas the base list, which will reset any in-progress reorders and potentially misplace items when multiple drag operations occur.val reorderableListState = rememberReorderableLazyListState(lazyListState) { from, to -> - reorderableTasks = tasks.toMutableList().apply { + reorderableTasks = reorderableTasks.toMutableList().apply { add(to.index, removeAt(from.index)) } }
🧹 Nitpick comments (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)
100-120: Verify reorder toggle button visibility logic.The reorder toggle button is only visible when there are 2+ categories but is disabled when the current category is empty. This creates a confusing UX where users see a disabled toggle with no clear reason. Consider either:
- Hiding the toggle when the current category is empty, or
- Showing a tooltip explaining why it's disabled
AnimatedVisibility( - visible = categories.size >= 2 + visible = categories.size >= 2 && !state.tasks[state.currentCategory].isNullOrEmpty() ) { FilledTonalIconToggleButton( checked = editState, shapes = IconToggleButtonShapes( shape = CircleShape, checkedShape = MaterialTheme.shapes.small, pressedShape = MaterialTheme.shapes.extraSmall, ), - onCheckedChange = { editState = it }, - enabled = !state.tasks[state.currentCategory].isNullOrEmpty() + onCheckedChange = { editState = it } ) { Icon( imageVector = Icons.Rounded.Reorder, contentDescription = null ) } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt(6 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
TaskCard(27-88)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
🔇 Additional comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)
61-67: LGTM!The function signature and return type correctly follow the new pattern with PageFill wrapper, aligning with the broader UI reorganization.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
86-92: LGTM!The function signature and PageFill wrapper integration align well with the new UI structure.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
Show resolved
Hide resolved
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (4)
82-89: Replace arbitrary delay with deterministic selection of the new “first” category
delay(200)is a race-prone heuristic; it neither guarantees the DB has emitted the new state nor that_state.tasksreflects the intended ordering. Compute the new current category deterministically fromaction.mapping(lowest index) and update state immediately. Fall back to existing keys if the mapping is partial/empty.Apply this diff:
- delay(200) - - _state.update { - it.copy( - currentCategory = it.tasks.keys.firstOrNull() - ) - } + val newCurrent = action.mapping.minByOrNull { it.first }?.second + _state.update { it.copy(currentCategory = newCurrent ?: it.tasks.keys.firstOrNull()) }Alternative (if you truly need to wait for the post-DB emission): await the next emission instead of sleeping.
// Suspends until the next tasks snapshot is available after the reorder. val firstAfterReorder = repo.getTasksFlow().map { it.keys.firstOrNull() }.first() _state.update { it.copy(currentCategory = firstAfterReorder) }
94-101: Remove sleep and redundant reassignment after delete; it fights with existing state logicAfter
deleteCategory(action.category), you sleep and then force-select the first key. This competes withdeleteCategory(...)’s own update and can cause flicker and incorrect selection. Prefer a single source of truth for this invariant.Apply this diff:
- delay(200) - - _state.update { - it.copy( - currentCategory = it.tasks.keys.firstOrNull() - ) - }Follow-up: rely on the invariant maintained in
observeTasks()(see next comment) and/or indeleteCategory(...).
118-124: Maintain invariant: currentCategory must be null or present in tasks; fix stale-selection bugYou only correct
currentCategorywhen it is null. If the selected category was deleted or renamed, it can become absent yet non-null, leaving state pointing to a non-existent key. Tighten the condition to also handle “not in keys”.Apply this diff:
- if (_state.value.currentCategory == null) { - _state.update { - it.copy( - currentCategory = tasks.keys.firstOrNull() - ) - } - } + val current = _state.value.currentCategory + if (current == null || current !in tasks.keys) { + _state.update { it.copy(currentCategory = tasks.keys.firstOrNull()) } + }Note: This change eliminates the need for time-based sleeps elsewhere.
1-161: Ensure Category equality depends only on a stable identifierThe
tasks: Map<Category, List<Task>>usesCategoryas the key, but sinceCategoryis defined as a Kotlindata classwith all four primary‐constructor properties (id,name,index,color), its autogeneratedequals/hashCodeinclude mutable fields likename,index, andcolor. When you reorder (index) or rename (name) a category, newCategoryinstances no longer match the old map keys, breaking lookups and UI filters.Action items:
- In
app/src/main/java/com/shub39/grit/tasks/domain/Category.kt, overrideequalsandhashCodeto use only the immutableidfield.- Alternatively, separate identity from presentation: keep a plain
CategoryIdfor map keys and a distinct value class (or DTO) for display properties (name,index,color).- Update any code that relies on
Categoryequality (e.g., task lookups, current‐category tracking) to use the new identity‐only semantics.app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
587-595: Fix index when adding to a different category.The new task’s
indexis derived fromstate.currentCategory, which is wrong if the user selected another category in the sheet. Compute the size of the target category instead.- index = state.tasks[state.currentCategory]?.size ?: 0 + index = run { + val target = state.tasks.keys.firstOrNull { it.id == newTaskCategoryId } + state.tasks[target]?.size ?: 0 + }app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)
294-301: Description field validates the wrong variable.Both the label text and
isErrorfor the description field referencenewHabitNameinstead ofnewHabitDescription.- label = { - if (newHabitName.length <= 50) { + label = { + if (newHabitDescription.length <= 50) { Text(text = stringResource(id = R.string.description)) } else { Text(text = stringResource(id = R.string.too_long)) } }, - isError = newHabitName.length > 50 + isError = newHabitDescription.length > 50
♻️ Duplicate comments (3)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)
262-267: Reorder uses the working list, not the original snapshot. Good catch.The move-on-complete path now starts from
reorderableTasks.toMutableList(), preserving any in-progress reorders. This addresses the earlier review concern.
292-292: Spacer height fix looks good.
Spacer(modifier = Modifier.height(60.dp))correctly reserves space to avoid FAB overlap.app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)
68-70: Missing import for draggableHandle (compile error persists).
Modifier.draggableHandle(...)is used at Line 167 but its extension import is still missing.Apply this diff to fix the import:
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.draggableHandle
🧹 Nitpick comments (13)
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (3)
77-81: Batch category reorders atomically in the repo/DAOLooping
upsertCategory(...)for each mapping entry issues N separate writes and N flow emissions. Wrap the reorder in a single repository API executed in a DB transaction (e.g., Room@Transaction) to ensure atomicity and reduce intermediate UI states.Proposed repository API:
suspend fun reorderCategories(mapping: List<Pair<Int, Category>>)Then call
repo.reorderCategories(action.mapping)here and drop the per-item loop.
151-161: Avoid forcing a non-empty key; prefer safe fallback and let observeTasks repairUsing
it.tasks.keys.first()assumes the map is non-empty at that instant. While it currently runs before the repo deletion, usingfirstOrNull()is safer and aligns with the invariant fix above.Apply this diff:
- currentCategory = it.tasks.keys.first() + currentCategory = it.tasks.keys.firstOrNull()
61-69: UX nit: selecting the “first” category after Add may not be the newly added categoryAfter adding, you set
currentCategorytoit.tasks.keys.firstOrNull(), which may not be the new category. If the intent is to focus the just-created category, set it explicitly toaction.category(or its persisted copy with ID).Potential tweak:
_state.update { it.copy(currentCategory = action.category /* or persisted copy */) }Or let the invariant in
observeTasks()handle selection if you prefer passive behavior.app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (4)
212-221: Stabilize reorderableTasks initialization and syncing.Using
remember(category.id, state.tasks.values)can reset local reorders when unrelated categories change, and keys based onvaluesare brittle. Initialize on category change and explicitly sync when that category’s list updates.- var reorderableTasks by remember(category.id, state.tasks.values) { mutableStateOf( - state.tasks[category] ?: emptyList() - ) } + var reorderableTasks by remember(category.id) { + mutableStateOf(state.tasks[category].orEmpty()) + } + LaunchedEffect(state.tasks[category]) { + // Sync local list when backing data for the current category changes. + reorderableTasks = state.tasks[category].orEmpty() + } val reorderableListState = rememberReorderableLazyListState(lazyListState) { from, to -> reorderableTasks = reorderableTasks.toMutableList().apply { add(to.index, removeAt(from.index)) } }To verify no regressions, try: start a drag reorder without dropping, change another category’s tasks (e.g., complete a task there), then return—local reorder state for the active category should remain intact.
481-489: No-op KeyboardActions in edit sheet; switch to a meaningful IME action.
newTitle.plus("\n")doesn’t mutatenewTitle, soonAnyis a no-op. Recommend usingImeAction.Doneand hiding the keyboard (or handling save).- keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.None - ), - keyboardActions = KeyboardActions( - onAny = { - newTitle.plus("\n") - } - ), + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { keyboardController?.hide() } + ),
569-577: No-op KeyboardActions in add sheet; mirror the edit-sheet fix.Same issue as above—
newTask.plus("\n")is ineffective. Use a real IME action.- keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.None - ), - keyboardActions = KeyboardActions( - onAny = { - newTask.plus("\n") - } - ), + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { keyboardController?.hide() } + ),
102-108: Minor: smoother FAB visibility threshold.Using only
firstVisibleItemIndexcan flicker at the top. Consider also checkingfirstVisibleItemScrollOffset.- val fabVisible by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex <= 0 - } - } + val fabVisible by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex == 0 && + lazyListState.firstVisibleItemScrollOffset < 10 + } + }app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (6)
88-96: Prevent unnecessary resets of the local reordered list.
remember(state)will resetreorderableHabitswhenever thestateobject identity changes (which can be frequent). Scope the memory key to the source of the list instead.- var reorderableHabits by remember(state) { mutableStateOf(habits) } + var reorderableHabits by remember(state.habitsWithStatuses) { mutableStateOf(habits) }
98-99: Use exitUntilCollapsed for a flexible top app bar (optional).
MediumFlexibleTopAppBartypically pairs better withTopAppBarDefaults.exitUntilCollapsedScrollBehavior();enterAlwayscan feel jumpy in nested scroll scenarios.- val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
273-282: Align input thresholds with button enablement.Title label/error use 20 chars as the limit; button uses
< 20. This disables the Add button when length is exactly 20. Similarly, use<= 50for description to match its label/error.- enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50, + enabled = newHabitName.isNotBlank() && newHabitName.length <= 20 && newHabitDescription.length <= 50,Also applies to: 377-381
404-405: Prefer explicit Spacer height over padding for vertical spacing.
Spacer(Modifier.padding(vertical = 4.dp))results in 8.dp of vertical space and is less clear than a fixed height.- Spacer(modifier = Modifier.padding(vertical = 4.dp)) + Spacer(modifier = Modifier.height(8.dp))If you actually intended 4.dp, use
Modifier.height(4.dp)instead.
336-357: Optional: Order day chips by the user’s configured starting day.Currently uses
DayOfWeek.entries, which always starts on Monday. Consider rotating the sequence so chips start atstate.startingDay, matching the calendar UX.Example (outside this block for clarity):
val orderedDays = generateSequence(state.startingDay) { it.plus(1).takeIf { next -> next != state.startingDay } } .toList() orderedDays.forEach { dayOfWeek -> /* ToggleButton(...) */ }
360-372: Minor: Index for new item source of truth.
index = reorderableHabits.sizewill work, but consider usingstate.habitsWithStatuses.sizeto avoid any temporary divergence if local reorder state and VM state fall out of sync during recompositions.- index = reorderableHabits.size, + index = state.habitsWithStatuses.size,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt(3 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt(7 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt(6 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(8 hunks)app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt
- app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
TaskCard(27-88)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1)
HabitCard(44-235)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
🔇 Additional comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
307-311: Confirm availability of animateFloatingActionButton().
Modifier.animateFloatingActionButton(...)is not part of stable Material3 APIs in older Compose versions. Ensure your dependency bump includes the artifact/version that provides this extension.If your catalog pins Compose Material3, please verify the function exists by building locally. If unavailable, consider replacing with
AnimatedVisibility+animate*AsStateorAnimatedContentaround the FAB.app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)
25-41: ✔️ Expressive Material3 API and Reorderable Dependency Verified
- The version catalog (
gradle/libs.versions.toml) pinsandroidx.compose.material3:material3to 1.5.0-alpha02, which includes the ExperimentalMaterial3ExpressiveApi and theMediumFlexibleTopAppBar,MediumFloatingActionButton,IconToggleButtonShapes, andButtonShapescomponents.- The
sh.calvin.reorderable:reorderablelibrary is declared in the same catalog, so no missing dependency issues.- There is no usage or import of
FlowRowin HabitsList.kt, so no additional@OptIn(ExperimentalLayoutApi::class)is required here.All checks pass; no changes needed.
| LazyColumn( | ||
| state = lazyListState, | ||
| modifier = Modifier.fillMaxSize(), | ||
| contentPadding = PaddingValues(16.dp), | ||
| verticalArrangement = Arrangement.spacedBy(8.dp) | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reordering won’t work without attaching Modifier.reorderable to LazyColumn.
You created the reorderableListState and items are wrapped in ReorderableItem, but the list itself isn’t wired to the drag system. Without Modifier.reorderable(reorderableListState), drag gestures will not produce reordering.
Apply this diff to attach the modifier:
- LazyColumn(
- state = lazyListState,
- modifier = Modifier.fillMaxSize(),
+ LazyColumn(
+ state = lazyListState,
+ modifier = Modifier
+ .fillMaxSize()
+ .reorderable(reorderableListState),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {And add the import (separate from the earlier draggableHandle import):
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.reorderable📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| LazyColumn( | |
| state = lazyListState, | |
| modifier = Modifier.fillMaxSize(), | |
| contentPadding = PaddingValues(16.dp), | |
| verticalArrangement = Arrangement.spacedBy(8.dp) | |
| ) { | |
| import sh.calvin.reorderable.ReorderableItem | |
| import sh.calvin.reorderable.rememberReorderableLazyListState | |
| import sh.calvin.reorderable.reorderable |
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt
around lines 141 to 146, the LazyColumn is not wired to the drag/reorder system;
attach Modifier.reorderable(reorderableListState) to the LazyColumn's modifier
chain (e.g., Modifier.fillMaxSize().reorderable(reorderableListState).then(other
modifiers) or add it before contentPadding) so the ReorderableItem and
reorderableListState receive drag gestures, and add the corresponding import for
Modifier.reorderable from the reorderable library (separate from the
draggableHandle import).
| AnimatedVisibility( | ||
| visible = state.tasks.values.isNotEmpty() | ||
| ) { | ||
| FilledTonalIconToggleButton( | ||
| checked = editState, | ||
| shapes = IconToggleButtonShapes( | ||
| shape = CircleShape, | ||
| checkedShape = MaterialTheme.shapes.small, | ||
| pressedShape = MaterialTheme.shapes.extraSmall, | ||
| ), | ||
| onCheckedChange = { editState = it }, | ||
| enabled = !state.tasks[state.currentCategory].isNullOrEmpty() | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Rounded.Reorder, | ||
| contentDescription = null | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Show “reorder” toggle only when the current category has tasks.
Visibility currently keys off the entire map (state.tasks.values.isNotEmpty()), so the toggle can appear even when the selected category is empty. Bind visibility to the currently selected category, consistent with the existing enabled check.
- AnimatedVisibility(
- visible = state.completedTasks.isNotEmpty()
- ) {
+ AnimatedVisibility(
+ visible = state.completedTasks.isNotEmpty()
+ ) {
...
- AnimatedVisibility(
- visible = state.tasks.values.isNotEmpty()
- ) {
+ AnimatedVisibility(
+ visible = !state.tasks[state.currentCategory].isNullOrEmpty()
+ ) {
FilledTonalIconToggleButton(
checked = editState,
...
enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
) {Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 144 to 162, the AnimatedVisibility currently checks the whole tasks
map (state.tasks.values.isNotEmpty()), causing the reorder toggle to show even
when the selected category is empty; change the visibility predicate to target
the currently selected category (e.g., check state.tasks[state.currentCategory]
for null/empty) so the toggle only appears when the current category has tasks,
keeping the existing enabled check for redundancy and handling null safely.
| item { | ||
| FilledTonalIconButton( | ||
| onClick = { showCategoryAddSheet = true }, | ||
| enabled = !editState | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Rounded.Add, | ||
| contentDescription = "Add Category" | ||
| ) | ||
| } | ||
|
|
||
| item { | ||
| Row( | ||
| horizontalArrangement = Arrangement.spacedBy(8.dp) | ||
| ) { | ||
| AssistChip( | ||
| onClick = { showCategoryAddSheet = true }, | ||
| enabled = !editState, | ||
| colors = AssistChipDefaults.assistChipColors( | ||
| containerColor = MaterialTheme.colorScheme.primaryContainer, | ||
| labelColor = MaterialTheme.colorScheme.onPrimaryContainer | ||
| ), | ||
| border = AssistChipDefaults.assistChipBorder(false), | ||
| label = { | ||
| Icon( | ||
| painter = painterResource(R.drawable.round_add_24), | ||
| contentDescription = "Add Category" | ||
| ) | ||
| } | ||
| ) | ||
|
|
||
| AssistChip( | ||
| onClick = onNavigateToEditCategories, | ||
| enabled = !editState, | ||
| colors = AssistChipDefaults.assistChipColors( | ||
| containerColor = MaterialTheme.colorScheme.primaryContainer, | ||
| labelColor = MaterialTheme.colorScheme.onPrimaryContainer | ||
| ), | ||
| border = AssistChipDefaults.assistChipBorder(false), | ||
| label = { | ||
| Icon( | ||
| painter = painterResource(R.drawable.baseline_edit_square_24), | ||
| contentDescription = "Edit Categories" | ||
| ) | ||
| } | ||
| ) | ||
| } | ||
| FilledTonalIconButton( | ||
| onClick = onNavigateToEditCategories, | ||
| enabled = !editState | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Rounded.Edit, | ||
| contentDescription = "Edit Categories" | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two buttons inside a single LazyRow item will overlap and bypass spacedBy(). Split them into two items.
Placing multiple composables in one item { ... } for a LazyRow causes them to render within the same item without the row-level spacing. Define separate items (or wrap in a Row) so Arrangement.spacedBy(8.dp) applies.
- item {
- FilledTonalIconButton(
- onClick = { showCategoryAddSheet = true },
- enabled = !editState
- ) {
- Icon(
- imageVector = Icons.Rounded.Add,
- contentDescription = "Add Category"
- )
- }
-
- FilledTonalIconButton(
- onClick = onNavigateToEditCategories,
- enabled = !editState
- ) {
- Icon(
- imageVector = Icons.Rounded.Edit,
- contentDescription = "Edit Categories"
- )
- }
- }
+ item {
+ FilledTonalIconButton(
+ onClick = { showCategoryAddSheet = true },
+ enabled = !editState
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = "Add Category"
+ )
+ }
+ }
+ item {
+ FilledTonalIconButton(
+ onClick = onNavigateToEditCategories,
+ enabled = !editState
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Edit,
+ contentDescription = "Edit Categories"
+ )
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| item { | |
| FilledTonalIconButton( | |
| onClick = { showCategoryAddSheet = true }, | |
| enabled = !editState | |
| ) { | |
| Icon( | |
| imageVector = Icons.Rounded.Add, | |
| contentDescription = "Add Category" | |
| ) | |
| } | |
| item { | |
| Row( | |
| horizontalArrangement = Arrangement.spacedBy(8.dp) | |
| ) { | |
| AssistChip( | |
| onClick = { showCategoryAddSheet = true }, | |
| enabled = !editState, | |
| colors = AssistChipDefaults.assistChipColors( | |
| containerColor = MaterialTheme.colorScheme.primaryContainer, | |
| labelColor = MaterialTheme.colorScheme.onPrimaryContainer | |
| ), | |
| border = AssistChipDefaults.assistChipBorder(false), | |
| label = { | |
| Icon( | |
| painter = painterResource(R.drawable.round_add_24), | |
| contentDescription = "Add Category" | |
| ) | |
| } | |
| ) | |
| AssistChip( | |
| onClick = onNavigateToEditCategories, | |
| enabled = !editState, | |
| colors = AssistChipDefaults.assistChipColors( | |
| containerColor = MaterialTheme.colorScheme.primaryContainer, | |
| labelColor = MaterialTheme.colorScheme.onPrimaryContainer | |
| ), | |
| border = AssistChipDefaults.assistChipBorder(false), | |
| label = { | |
| Icon( | |
| painter = painterResource(R.drawable.baseline_edit_square_24), | |
| contentDescription = "Edit Categories" | |
| ) | |
| } | |
| ) | |
| } | |
| FilledTonalIconButton( | |
| onClick = onNavigateToEditCategories, | |
| enabled = !editState | |
| ) { | |
| Icon( | |
| imageVector = Icons.Rounded.Edit, | |
| contentDescription = "Edit Categories" | |
| ) | |
| } | |
| } | |
| // … other items in the LazyRow … | |
| // Add Category button as its own item | |
| item { | |
| FilledTonalIconButton( | |
| onClick = { showCategoryAddSheet = true }, | |
| enabled = !editState | |
| ) { | |
| Icon( | |
| imageVector = Icons.Rounded.Add, | |
| contentDescription = "Add Category" | |
| ) | |
| } | |
| } | |
| // Edit Categories button as its own item | |
| item { | |
| FilledTonalIconButton( | |
| onClick = onNavigateToEditCategories, | |
| enabled = !editState | |
| ) { | |
| Icon( | |
| imageVector = Icons.Rounded.Edit, | |
| contentDescription = "Edit Categories" | |
| ) | |
| } | |
| } | |
| // … following items in the LazyRow … |
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 183 to 203, two FilledTonalIconButton composables are placed inside
a single LazyRow item which prevents the LazyRow's Arrangement.spacedBy(...)
from applying and causes overlap; split them into separate LazyRow item { ... }
blocks (one per button) so the row spacing applies, or alternatively wrap them
in a Row and apply proper horizontal spacing inside that Row if you want them
grouped, keeping the same onClick and enabled properties.
| modifier = Modifier.draggableHandle( | ||
| onDragStopped = { | ||
| onAction( | ||
| TaskPageAction.ReorderTasks( | ||
| reorderableTasks.mapIndexed { index, task -> index to task }) | ||
| ) | ||
| } | ||
| ) | ||
| }, | ||
| shape = RoundedCornerShape(cardCorners), | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) | ||
| .combinedClickable( | ||
| // change status on click | ||
| onClick = { | ||
| if (!editState) { | ||
| task.status = !task.status | ||
|
|
||
| onAction(TaskPageAction.UpsertTask(task)) | ||
|
|
||
| if (task.status) { | ||
| reorderableTasks = tasks.toMutableList().apply { | ||
| add(tasks.size - 1, removeAt(index)) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing import for draggableHandle extension.
Modifier.draggableHandle is defined in the sh.calvin.reorderable package but isn’t imported here, which will fail compilation.
Add this import near the other sh.calvin.reorderable.* imports:
import sh.calvin.reorderable.draggableHandle🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 239 to 247, the code uses Modifier.draggableHandle but the
extension import is missing; add the import
sh.calvin.reorderable.draggableHandle alongside the other
sh.calvin.reorderable.* imports so the draggableHandle extension resolves and
the file compiles.
| onClick = { | ||
| if (!editState) { | ||
| task.status = !task.status | ||
|
|
||
| onAction(TaskPageAction.UpsertTask(task)) | ||
|
|
||
| if (task.status) { | ||
| reorderableTasks = | ||
| reorderableTasks.toMutableList().apply { | ||
| add( | ||
| reorderableTasks.size - 1, | ||
| removeAt(index) | ||
| ) | ||
| } | ||
|
|
||
| onAction(TaskPageAction.ReorderTasks(reorderableTasks.mapIndexed { index, task -> index to task })) | ||
| } | ||
| } | ||
| }, | ||
| // edit on click and hold | ||
| onLongClick = { | ||
| if (!editState) { | ||
| editTask = task | ||
| onAction( | ||
| TaskPageAction.ReorderTasks( | ||
| reorderableTasks.mapIndexed { index, task -> index to task }) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid in-place mutation of Task; emit a copied value and keep local list consistent.
Directly toggling task.status mutates state objects held by Compose, which can lead to stale snapshots. Prefer copy(...), dispatch the update, and update the local list with the new instance.
- if (!editState) {
- task.status = !task.status
-
- onAction(TaskPageAction.UpsertTask(task))
-
- if (task.status) {
- reorderableTasks =
- reorderableTasks.toMutableList().apply {
- add(
- reorderableTasks.size - 1,
- removeAt(index)
- )
- }
-
- onAction(
- TaskPageAction.ReorderTasks(
- reorderableTasks.mapIndexed { index, task -> index to task })
- )
- }
- }
+ if (!editState) {
+ val updated = task.copy(status = !task.status)
+ onAction(TaskPageAction.UpsertTask(updated))
+
+ if (updated.status) {
+ val next = reorderableTasks.toMutableList()
+ // Move updated item to end
+ next.removeAt(index)
+ next.add(next.size, updated)
+ reorderableTasks = next
+
+ onAction(
+ TaskPageAction.ReorderTasks(
+ reorderableTasks.mapIndexed { idx, t -> idx to t }
+ )
+ )
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onClick = { | |
| if (!editState) { | |
| task.status = !task.status | |
| onAction(TaskPageAction.UpsertTask(task)) | |
| if (task.status) { | |
| reorderableTasks = | |
| reorderableTasks.toMutableList().apply { | |
| add( | |
| reorderableTasks.size - 1, | |
| removeAt(index) | |
| ) | |
| } | |
| onAction(TaskPageAction.ReorderTasks(reorderableTasks.mapIndexed { index, task -> index to task })) | |
| } | |
| } | |
| }, | |
| // edit on click and hold | |
| onLongClick = { | |
| if (!editState) { | |
| editTask = task | |
| onAction( | |
| TaskPageAction.ReorderTasks( | |
| reorderableTasks.mapIndexed { index, task -> index to task }) | |
| ) | |
| } | |
| onClick = { | |
| if (!editState) { | |
| // Emit a fresh copy instead of mutating in-place | |
| val updated = task.copy(status = !task.status) | |
| onAction(TaskPageAction.UpsertTask(updated)) | |
| if (updated.status) { | |
| // Rebuild the local list with the new instance moved to the end | |
| val next = reorderableTasks.toMutableList() | |
| next.removeAt(index) | |
| next.add(next.size, updated) | |
| reorderableTasks = next | |
| onAction( | |
| TaskPageAction.ReorderTasks( | |
| reorderableTasks.mapIndexed { idx, t -> idx to t } | |
| ) | |
| ) | |
| } | |
| } | |
| // ... other onClick handlers | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 255 to 274, the code mutates task.status in-place which can break
Compose state snapshots; instead create a newTask = task.copy(status =
!task.status), call onAction(TaskPageAction.UpsertTask(newTask)), and replace
the item in the local list(s) (e.g. reorderableTasks = reorderableTasks.map { if
(it == task || it.id == task.id) newTask else it }.toMutableList()) so the UI
sees the new instance; when reordering, use the newTask instance in the moved
position and dispatch ReorderTasks with the updated list.
| import com.shub39.grit.tasks.presentation.TaskPageAction | ||
| import com.shub39.grit.tasks.presentation.TaskPageState | ||
| import kotlinx.coroutines.Job | ||
| import kotlinx.coroutines.delay |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid ad‑hoc time-based synchronization; remove delay import once delays are gone
Importing kotlinx.coroutines.delay foreshadows time-based waiting below. These sleeps make state updates nondeterministic and flaky under load or slow I/O.
Apply this once you remove the delay(...) usages:
-import kotlinx.coroutines.delay📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import kotlinx.coroutines.delay | |
| // (remove the now-unused import) |
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt around line
12, remove the import kotlinx.coroutines.delay and eliminate any ad-hoc
delay(...) usages; instead convert the code to use proper coroutine
synchronization (e.g., suspend functions that return when work is complete, use
MutableStateFlow/LiveData updates, await jobs or use async/await with structured
concurrency, or expose callbacks/completion handlers) and update tests to use
TestCoroutineDispatcher or appropriate idling mechanisms so no explicit sleeps
are needed.
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)
317-324: Fix description field validation using the wrong variableThe description field’s label/error conditions read
newHabitName.lengthinstead ofnewHabitDescription.length, leading to incorrect validation UI.- label = { - if (newHabitName.length <= 50) { + label = { + if (newHabitDescription.length <= 50) { Text(text = stringResource(id = R.string.description)) } else { Text(text = stringResource(id = R.string.too_long)) } }, - isError = newHabitName.length > 50 + isError = newHabitDescription.length > 50
♻️ Duplicate comments (2)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)
75-77: Missing import for draggableHandle still causes a compile error
This was flagged earlier and remains unresolved. Add the import soModifier.draggableHandleresolves.import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.draggableHandle
159-165: Attach Modifier.reorderable to LazyColumn to enable drag reorderingWithout this, drag gestures won’t be dispatched to the list.
- LazyColumn( - state = lazyListState, - modifier = Modifier.fillMaxSize(), + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxSize() + .reorderable(reorderableListState), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) {And add the import:
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.reorderable
🧹 Nitpick comments (7)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (4)
16-21: Verify icon availability and align visual styleDouble-check that
Icons.Rounded.CheckCircleOutlineexists in your icon pack. If it doesn’t, compilation will fail. Also consider keeping styles consistent (Outlined+Outlined or Rounded+Rounded) between the incomplete and complete states.Two safe alternatives:
Option A (Outlined for both):
- import androidx.compose.material.icons.rounded.CheckCircleOutline + import androidx.compose.material.icons.outlined.CheckCircleOutline- imageVector = if (!it) Icons.Outlined.Circle else Icons.Rounded.CheckCircleOutline, + imageVector = if (!it) Icons.Outlined.Circle else Icons.Outlined.CheckCircleOutline,Option B (Rounded for completed):
- import androidx.compose.material.icons.rounded.CheckCircleOutline + import androidx.compose.material.icons.rounded.CheckCircle- imageVector = if (!it) Icons.Outlined.Circle else Icons.Rounded.CheckCircleOutline, + imageVector = if (!it) Icons.Outlined.Circle else Icons.Rounded.CheckCircle,Also applies to: 111-113
63-77: Fix label mismatch in animateColorAsStateThe first
animateColorAsStatedrives content color but uses label"cardBackground". Rename to avoid confusion in inspection tools.- val cardContent by animateColorAsState( + val cardContent by animateColorAsState( targetValue = when (completed) { true -> MaterialTheme.colorScheme.onPrimaryContainer else -> MaterialTheme.colorScheme.onSurface }, - label = "cardBackground" + label = "cardContent" )
148-151: Avoid repeated O(n) scans; precompute dates and reuse
statusList.any { ... }is executed multiple times per visible day and you re-create a list forcountCurrentStreak. Precompute once and use a Set for O(1) membership checks.+import androidx.compose.runtime.rememberval weekState = rememberWeekCalendarState( startDate = habit.time.toLocalDate().minusMonths(12), endDate = LocalDate.now(), firstVisibleWeekDate = LocalDate.now(), firstDayOfWeek = startingDay ) + // Precompute date collections for performance + val dates = remember(statusList) { statusList.map { it.date } } + val dateSet = remember(dates) { dates.toHashSet() }- Text( - text = countCurrentStreak(statusList.map { it.date }).toString() - ) + Text(text = countCurrentStreak(dates).toString())- val done = statusList.any { it.date == weekDay.date } + val done = weekDay.date in dateSet- val donePrevious = - statusList.any { it.date == weekDay.date.minusDays(1) } - val doneAfter = - statusList.any { it.date == weekDay.date.plusDays(1) } + val donePrevious = (weekDay.date.minusDays(1) in dateSet) + val doneAfter = (weekDay.date.plusDays(1) in dateSet)Also applies to: 175-193, 222-231
228-232: Localize weekday labelUsing
dayOfWeek.toString().take(3)yields English, all-caps abbreviations. Prefer localized short names.+import java.time.format.TextStyle +import java.util.Locale- Text( - text = weekDay.date.dayOfWeek.toString().take(3), + Text( + text = weekDay.date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()), style = MaterialTheme.typography.bodySmall, color = if (done) MaterialTheme.colorScheme.onPrimary else cardContent )app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
401-402: Align max-length rules across UI statesButton enablement uses
<while labels use<=. Make them consistent so exactly-20/50-char inputs are allowed.- enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50, + enabled = newHabitName.isNotBlank() && newHabitName.length <= 20 && newHabitDescription.length <= 50,
95-99: Smooth FAB visibility with offset thresholdCurrently it hides as soon as the first item is not index 0. Consider also checking the scroll offset for a smoother experience.
- val fabVisible by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex == 0 - } - } + val fabVisible by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex == 0 && + lazyListState.firstVisibleItemScrollOffset < 24 + } + }
140-154: Add contentDescription for the reorder toggle for accessibilityThe toggle has
contentDescription = null, which is not ideal for screen readers.- FilledTonalIconToggleButton( + FilledTonalIconToggleButton( checked = editState, shapes = IconToggleButtonShapes( shape = CircleShape, checkedShape = MaterialTheme.shapes.small, pressedShape = MaterialTheme.shapes.extraSmall, ), onCheckedChange = { editState = it }, enabled = state.habitsWithStatuses.isNotEmpty() ) { - Icon( - imageVector = Icons.Rounded.Reorder, - contentDescription = null - ) + Icon( + imageVector = Icons.Rounded.Reorder, + contentDescription = "Toggle reorder mode" + ) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt(7 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1)
HabitCard(48-238)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)
317-324: Description field validates the wrong variable.
OutlinedTextFieldfor the description usesnewHabitName.lengthfor label/error checks instead ofnewHabitDescription.length. This breaks validation and UX.Apply this diff:
- label = { - if (newHabitName.length <= 50) { + label = { + if (newHabitDescription.length <= 50) { Text(text = stringResource(id = R.string.description)) } else { Text(text = stringResource(id = R.string.too_long)) } }, - isError = newHabitName.length > 50 + isError = newHabitDescription.length > 50
401-401: “Add” button can be disabled at valid limits and allows duplicates.
- Off-by-one: name length 20 and description length 50 should be allowed (consistent with your label/error checks), but the button uses
<and disables submission.- Duplicates: the button is enabled even when a habit with the same title exists.
Align with the field constraints and prevent duplicates:
- enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50, + enabled = newHabitName.isNotBlank() + && newHabitName.length <= 20 + && newHabitDescription.length <= 50 + && !isHabitPresent(),app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)
255-272: Use the existing ChangeCategory action when editing a categoryCalling
TaskPageAction.AddCategoryin the “Edit Category” sheet will create a new category rather than updating the one being edited. Since you’ve already defined aChangeCategory(val category: Category)action, dispatch that here to trigger an update (the ViewModel routes both Add and Change toupsertCategory, so ChangeCategory won’t duplicate).• File:
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt
• Location: replace the onClick block that currently dispatchesAddCategorySuggested diff:
- onAction( - TaskPageAction.AddCategory( - category.copy(name = name) - ) - ) + onAction( + TaskPageAction.ChangeCategory( + category.copy(name = name) + ) + )This ensures the existing category is updated rather than a new one added.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
587-595: Wrong index when adding to a non-current category
index = state.tasks[state.currentCategory]?.size ?: 0uses the current category size even when the user selected a differentnewTaskCategoryId, causing incorrect placement.Compute the target index from the chosen category:
- onAction( - TaskPageAction.UpsertTask( - Task( - categoryId = newTaskCategoryId, - title = newTask, - status = false, - index = state.tasks[state.currentCategory]?.size ?: 0 - ) - ) - ) + val targetCategory = state.tasks.keys.firstOrNull { it.id == newTaskCategoryId } + val targetIndex = targetCategory?.let { state.tasks[it]?.size } ?: 0 + onAction( + TaskPageAction.UpsertTask( + Task( + categoryId = newTaskCategoryId, + title = newTask, + status = false, + index = targetIndex + ) + ) + )app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (1)
122-126: Fix state handling: derived values aren’t remembered, LaunchedEffect reassignments won’t recomposeThese locals are plain vars; reassigning them in LaunchedEffect doesn’t trigger recomposition and values reset on every recomposition. Compute them with remember/derivedStateOf (or drop LaunchedEffect entirely).
Apply this diff to remove the bug and simplify:
- var lineChartData = prepareLineChartData(state.startingDay, statuses) - var weekDayData = prepareWeekDayData(statuses.map { it.date }, primary) - var currentStreak = countCurrentStreak(statuses.map { it.date }) - var bestStreak = countBestStreak(statuses.map { it.date }) + val lineChartData = remember(statuses, state.startingDay) { + prepareLineChartData(state.startingDay, statuses) + } + val weekDayData = remember(statuses, primary) { + prepareWeekDayData(statuses.map { it.date }, primary) + } + val currentStreak = remember(statuses) { + countCurrentStreak(statuses.map { it.date }) + } + val bestStreak = remember(statuses) { + countBestStreak(statuses.map { it.date }) + } @@ - LaunchedEffect(statuses) { - lineChartData = prepareLineChartData(state.startingDay, statuses) - currentStreak = countCurrentStreak(statuses.map { it.date }) - bestStreak = countBestStreak(statuses.map { it.date }) - weekDayData = prepareWeekDayData(statuses.map { it.date }, primary) - } + // No LaunchedEffect necessary; values above update when keys change.Also applies to: 143-148
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (1)
82-111: Stabilize chart colors: Random colors regenerate on recompositionUsing Random.nextFloat() each composition causes lines to change colors, hurting recognition. Seed by habit id and remember the mapping.
Apply this diff:
- val weeklyGraphData = state.habitsWithStatuses.entries.map { entry -> - val habit = entry.key - val statuses = entry.value - val color = Color( - red = Random.nextFloat(), - green = Random.nextFloat(), - blue = Random.nextFloat() - ).harmonize(primary, true).fixIfDisliked() - - Line( - label = habit.title, - values = prepareLineChartData(state.startingDay, statuses), - color = SolidColor(color), - dotProperties = DotProperties( - enabled = false, - color = SolidColor(color), - strokeWidth = 4.dp, - radius = 7.dp - ), - firstGradientFillColor = color.copy(alpha = 0.8f), - secondGradientFillColor = Color.Transparent, - popupProperties = PopupProperties( - enabled = false - ), - drawStyle = DrawStyle.Stroke( - width = 3.dp, - strokeStyle = StrokeStyle.Normal - ) - ) - } + val weeklyGraphData = remember(state.habitsWithStatuses.keys, primary) { + state.habitsWithStatuses.entries.map { (habit, statuses) -> + val seeded = Random(habit.id.hashCode()) + val color = Color( + red = seeded.nextFloat(), + green = seeded.nextFloat(), + blue = seeded.nextFloat() + ).harmonize(primary, true).fixIfDisliked() + + Line( + label = habit.title, + values = prepareLineChartData(state.startingDay, statuses), + color = SolidColor(color), + dotProperties = DotProperties( + enabled = false, + color = SolidColor(color), + strokeWidth = 4.dp, + radius = 7.dp + ), + firstGradientFillColor = color.copy(alpha = 0.8f), + secondGradientFillColor = Color.Transparent, + popupProperties = PopupProperties(enabled = false), + drawStyle = DrawStyle.Stroke(width = 3.dp, strokeStyle = StrokeStyle.Normal) + ) + } + }
♻️ Duplicate comments (6)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)
75-77: Missing import for draggableHandle causes a compile error.
Modifier.draggableHandleis referenced but its extension import is absent.Apply this diff to add the import alongside the other reorderable imports:
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.draggableHandle
159-164: Reordering won’t work without wiring the list with Modifier.reorderable.You create
reorderableListStateand wrap items withReorderableItem, butLazyColumnitself isn’t connected to the drag system. WithoutModifier.reorderable(reorderableListState), drag gestures won’t produce item moves.Apply this diff to attach the modifier:
- LazyColumn( - state = lazyListState, - modifier = Modifier.fillMaxSize(), + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxSize() + .reorderable(reorderableListState), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) {And ensure the import exists (separate from the draggableHandle import):
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.reorderableapp/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (4)
144-162: Show/enable reorder only when the current category has tasksVisibility uses
state.tasks.values.isNotEmpty(), which can be true even when the selected category is empty. Bind to the current category to avoid a dead toggle.- AnimatedVisibility( - visible = state.tasks.values.isNotEmpty() - ) { + AnimatedVisibility( + visible = !state.tasks[state.currentCategory].isNullOrEmpty() + ) { FilledTonalIconToggleButton( checked = editState, ... - enabled = !state.tasks[state.currentCategory].isNullOrEmpty() + enabled = !state.tasks[state.currentCategory].isNullOrEmpty() ) {
183-203: Two buttons in one LazyRow item break spacing; split into separate itemsPlacing both buttons inside a single
item { ... }ignoresspacedBy(8.dp)and may overlap.- item { - FilledTonalIconButton( - onClick = { showCategoryAddSheet = true }, - enabled = !editState - ) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Add Category" - ) - } - - FilledTonalIconButton( - onClick = onNavigateToEditCategories, - enabled = !editState - ) { - Icon( - imageVector = Icons.Rounded.Edit, - contentDescription = "Edit Categories" - ) - } - } + item { + FilledTonalIconButton( + onClick = { showCategoryAddSheet = true }, + enabled = !editState + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = "Add Category" + ) + } + } + item { + FilledTonalIconButton( + onClick = onNavigateToEditCategories, + enabled = !editState + ) { + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = "Edit Categories" + ) + } + }
239-247: Missing import for draggableHandle extension (compilation fails)
Modifier.draggableHandle(...)is used, butimport sh.calvin.reorderable.draggableHandleis not present.Add the import near your other
reorderableimports:import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import sh.calvin.reorderable.draggableHandle
253-284: Avoid in-place mutation of Task and fix off-by-one when moving completed items
- Mutating
task.statusin-place can confuse Compose state and snapshotting.- Moving to
reorderableTasks.size - 1places the item second-to-last, not last.Apply this safer, deterministic update:
- if (!editState) { - task.status = !task.status - - onAction(TaskPageAction.UpsertTask(task)) - - if (task.status) { - reorderableTasks = - reorderableTasks.toMutableList().apply { - add( - reorderableTasks.size - 1, - removeAt(index) - ) - } - - onAction( - TaskPageAction.ReorderTasks( - reorderableTasks.mapIndexed { index, task -> index to task }) - ) - } - } + if (!editState) { + val updated = task.copy(status = !task.status) + onAction(TaskPageAction.UpsertTask(updated)) + + val next = reorderableTasks.toMutableList() + next.removeAt(index) + if (updated.status) { + // Move to the end + next.add(next.size, updated) + reorderableTasks = next + onAction( + TaskPageAction.ReorderTasks( + reorderableTasks.mapIndexed { idx, t -> idx to t } + ) + ) + } else { + // Keep position but replace instance for Compose + next.add(index, updated) + reorderableTasks = next + } + }
🧹 Nitpick comments (7)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)
95-99: Make FAB visibility more robust to partial scroll.Only checking
firstVisibleItemIndex == 0keeps the FAB visible when the list is partially scrolled. Include the scroll offset for a crisper UX.- val fabVisible by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex == 0 - } - } + val fabVisible by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex == 0 && + lazyListState.firstVisibleItemScrollOffset == 0 + } + }
128-133: Localize the “completed” counter using a formatted string.String concatenation harms localization. Prefer a resources-backed format string with placeholders.
Proposed code (requires a new string resource):
- Text( - text = "${state.completedHabits.size}/${state.habitsWithStatuses.size} " + stringResource( - R.string.completed - ) - ) + Text( + text = stringResource( + R.string.completed_count, + state.completedHabits.size, + state.habitsWithStatuses.size + ) + )Add to
res/values/strings.xml:<string name="completed_count">%1$d/%2$d completed</string>I can open a follow-up PR to add the resource for all locales.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (3)
109-127: Reorder toggle enablement condition is inconsistent with visibilityVisibility requires at least two categories (
categories.size >= 2), butenabled = state.tasks.keys.isNotEmpty()only checks that categories exist and says nothing about tasks. If the intent is to allow reordering when there are at least two categories, the enable check should match the visibility (or be omitted).Align the enablement with visibility:
- onCheckedChange = { editState = it }, - enabled = state.tasks.keys.isNotEmpty() + onCheckedChange = { editState = it }, + enabled = categories.size >= 2If you actually want the toggle disabled when there are no tasks at all, consider:
enabled = state.tasks.values.any { it.isNotEmpty() }
74-80: Emit ReorderCategories once on drag stop (reduce churn and state thrash)You dispatch
ReorderCategoriesinside the move callback, firing on every drag frame. Prefer updating local state during drag and dispatching once when the user releases.
- Keep the local list mutation in the move callback.
- Move the
onAction(TaskPageAction.ReorderCategories(...))emission to the handle’sonDragStopped.Apply these diffs:
Within rememberReorderableLazyListState:
val reorderableListState = rememberReorderableLazyListState(listState) { from, to -> categories = categories.toMutableList().apply { add(to.index, removeAt(from.index)) } - - onAction(TaskPageAction.ReorderCategories(categories.mapIndexed { index, category -> index to category })) }At the drag handle:
- FilledTonalIconButton( - onClick = {}, - modifier = Modifier.draggableHandle() - ) { + FilledTonalIconButton( + onClick = {}, + modifier = Modifier.draggableHandle( + onDragStopped = { + onAction( + TaskPageAction.ReorderCategories( + categories.mapIndexed { idx, c -> idx to c } + ) + ) + } + ) + ) {Also applies to: 169-179
146-147: Use plurals for task count to handle singular vs plural properlyText currently concatenates a number with
stringResource(R.string.tasks). This will read awkwardly for “1 tasks”.If you have a plurals resource, prefer:
val count = state.tasks[category]?.size ?: 0 Text(text = pluralStringResource(R.plurals.tasks, count, count))app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
122-125: Use plurals for “items completed” to handle singular correctly
"${state.completedTasks.size} " + stringResource(R.string.items_completed)will read “1 items completed”.Prefer a plurals resource (example):
val n = state.completedTasks.size Text(text = pluralStringResource(R.plurals.items_completed, n, n))app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)
42-63: Optional: Provide a graceful pre-Android 12 fallbackOn pre-S devices, content is not shown at all. Consider a tinted scrim with a lock icon/short copy instead of an empty area to keep layout consistent.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt(1 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt(8 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt(4 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt(5 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt(11 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt(7 hunks)app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
TaskCard(27-88)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
PageFill(11-20)app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1)
HabitCard(49-239)app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
Empty(20-45)
🔇 Additional comments (9)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)
109-126: LGTM: Top bar + nested scroll integration is cleanThe
enterAlwaysScrollBehaviorwiring withLargeFlexibleTopAppBarand nested scroll looks correct and keeps the FAB logic straightforward.app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)
30-41: LGTM: Card scaffolding and title styling look solidUsing MaterialTheme shapes/typography and primary color for the title is consistent with M3 guidelines.
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (3)
70-72: LGTM: import path updates align with the reorgComponent and utility imports reflect the new packages; matches usages below.
Also applies to: 79-83
469-473: LGTM: gating WeekDayBreakdown behind subscription via AnalyticsCardThe canSeeContent flag is correctly wired to the paywall action.
514-518: LGTM: CalendarMap paywall wiringcanSeeContent and onPlusClick are consistent with WeekDayBreakdown.
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (4)
120-131: LGTM: top app bar migration with nested scrollMediumFlexibleTopAppBar + enterAlways scroll behavior is correctly applied with nestedScroll.
Also applies to: 126-146
182-185: LGTM: WeekDayBreakdown behind paywallcanSeeContent and onPlusClick usage matches the new AnalyticsCard API.
229-233: LGTM: WeeklyGraph paywall + paddingConsistent with WeekDayBreakdown; inner padding improves chart legibility near the card edges.
Also applies to: 255-260
277-279: LGTM: HabitHeatMap spacing and primary-accented headersBottom/content padding reduce clipping; using primary for headers/initials improves visual hierarchy.
Also applies to: 288-288, 305-305
| if (!canSeeContent) { | ||
| Box( | ||
| contentAlignment = Alignment.Center, | ||
| modifier = Modifier | ||
| .heightIn(max = 300.dp) | ||
| .fillMaxWidth() | ||
| ) { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| Box( | ||
| modifier = Modifier | ||
| .matchParentSize() | ||
| .blur( | ||
| radius = 10.dp, | ||
| edgeTreatment = BlurredEdgeTreatment.Unbounded | ||
| ) | ||
| ) { content() } | ||
| } | ||
|
|
||
| Button( | ||
| onClick = onPlusClick | ||
| ) { Text(text = stringResource(R.string.unlock_plus)) } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Block interaction and accessibility when content is locked
When canSeeContent is false on API 31+, the blurred content is still present in the tree. TalkBack can read it, and underlying charts/calendars may still receive gestures unless each consumer disables them. Add: (1) a full-size, non-indicating clickable/spacer overlay to consume touches, and (2) clear semantics on the blurred content to avoid a11y leakage.
Apply this diff:
@@
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
@@
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
@@
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
@@
if (!canSeeContent) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.heightIn(max = 300.dp)
.fillMaxWidth()
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Box(
modifier = Modifier
.matchParentSize()
.blur(
radius = 10.dp,
edgeTreatment = BlurredEdgeTreatment.Unbounded
)
- ) { content() }
+ .clearAndSetSemantics { }
+ ) { content() }
}
+ // Swallow all pointer input over the locked area and optionally route to paywall
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = onPlusClick
+ ) { }
+ )
+
Button(
onClick = onPlusClick
) { Text(text = stringResource(R.string.unlock_plus)) }
}
} else {
content()
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt
around lines 42–63, when canSeeContent is false the blurred content remains in
the composition and is exposed to TalkBack and input; fix by wrapping the
blurred content with clear semantics (use clearAndSetSemantics {} so it provides
no accessible content) and add a full-size, non-indicating overlay that consumes
all touches (e.g., a matchParentSize modifier with a clickable that uses
indication = null and a fresh MutableInteractionSource, or a pointerInput that
simply consumes events) so underlying charts/calendars no longer receive
gestures. Ensure the overlay is rendered above the blurred content and the
semantics change is applied to the blurred container only.
| AnimatedVisibility(editState) { | ||
| FilledTonalIconButton( | ||
| onClick = {}, | ||
| modifier = Modifier.draggableHandle() | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Rounded.DragIndicator, | ||
| contentDescription = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing import for draggableHandle leads to compile error
Modifier.draggableHandle() is used but sh.calvin.reorderable.draggableHandle isn’t imported. The file won’t compile.
Apply this diff to add the missing import near your other reorderable imports:
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| AnimatedVisibility(editState) { | |
| FilledTonalIconButton( | |
| onClick = {}, | |
| modifier = Modifier.draggableHandle() | |
| ) { | |
| Icon( | |
| imageVector = Icons.Rounded.DragIndicator, | |
| contentDescription = null | |
| // … other imports … | |
| import sh.calvin.reorderable.ReorderableItem | |
| import sh.calvin.reorderable.rememberReorderableLazyListState | |
| import sh.calvin.reorderable.draggableHandle | |
| // … rest of file … |
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt
around lines 169 to 176, the code calls Modifier.draggableHandle() but the
import sh.calvin.reorderable.draggableHandle is missing which causes a compile
error; add the missing import alongside the other reorderable imports at the top
of the file (import sh.calvin.reorderable.draggableHandle) so
Modifier.draggableHandle() resolves correctly.
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (1)
523-530: Bug:todayshouldn’t be nullable here (used as non-null in comparisons)Inside
CalendarMapyou compareday.datewithtoday. Withtoday: LocalDate?, those operators won’t compile. Maketodaynon-null.-private fun CalendarMap( +private fun CalendarMap( state: HabitPageState, onAction: (HabitsPageAction) -> Unit, calendarState: CalendarState, statuses: List<HabitStatus>, - today: LocalDate?, + today: LocalDate, currentHabit: Habit, primary: Color )
🧹 Nitpick comments (5)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (5)
126-128: Avoid!!on derived state; handle missing analytics habit gracefullyCurrent code will crash if
analyticsHabitIddoesn’t exist inhabitsWithStatuses. Prefer a safe lookup with an early-return UX fallback.Apply this diff to harden lookups:
- val currentHabit = state.habitsWithStatuses.keys.find { it.id == state.analyticsHabitId }!! - val statuses = state.habitsWithStatuses[currentHabit]!! + val currentHabit = state.habitsWithStatuses.keys.firstOrNull { it.id == state.analyticsHabitId } + ?: run { + onNavigateBack() + return@PageFill + } + val statuses = state.habitsWithStatuses[currentHabit].orEmpty()If navigating back isn’t desired, show an inline error state instead of returning.
129-133: Replace local vars + LaunchedEffect with remember/derivedStateOf for correctness and efficiencyThese values are derived from
statusesandstartingDay. Using plainvar+ assigning insideLaunchedEffectdoesn’t trigger recomposition and duplicates work. Compute once per dependency change.- var lineChartData = prepareLineChartData(state.startingDay, statuses) - var weekDayData = prepareWeekDayData(statuses.map { it.date }, primary) - var currentStreak = countCurrentStreak(statuses.map { it.date }) - var bestStreak = countBestStreak(statuses.map { it.date }) + val lineChartData by remember(statuses, state.startingDay) { + derivedStateOf { prepareLineChartData(state.startingDay, statuses) } + } + val weekDayData by remember(statuses, primary) { + derivedStateOf { prepareWeekDayData(statuses.map { it.date }, primary) } + } + val currentStreak by remember(statuses) { + derivedStateOf { countCurrentStreak(statuses.map { it.date }) } + } + val bestStreak by remember(statuses) { + derivedStateOf { countBestStreak(statuses.map { it.date }) } + } @@ - LaunchedEffect(statuses) { - lineChartData = prepareLineChartData(state.startingDay, statuses) - currentStreak = countCurrentStreak(statuses.map { it.date }) - bestStreak = countBestStreak(statuses.map { it.date }) - weekDayData = prepareWeekDayData(statuses.map { it.date }, primary) - } + // derivedStateOf handles recomputation when dependencies change; no side-effects neededAdd (outside the ranges above), if missing:
import androidx.compose.runtime.derivedStateOfAlso applies to: 150-155
163-213: Minor polish: consider settingcontainerColorfor initial state parityYou set
scrolledContainerColorbut notcontainerColor. If surfaces differ, a color jump can occur before scrolling.- colors = TopAppBarDefaults.topAppBarColors( - scrolledContainerColor = MaterialTheme.colorScheme.surface - ), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface + ),
559-559: Prefer enum comparison over string literal for day positionComparing
day.position.nameto"MonthDate"is brittle. Use the enum directly.- if (day.position.name == "MonthDate") { + if (day.position == DayPosition.MonthDate) {Add (outside this range), if not already:
import com.kizitonwose.calendar.core.DayPosition
560-604: Optimize date lookups: precompute a Set for O(1) membership
statuses.any { it.date == … }is called repeatedly indayContentand scales poorly with history size. Use arememberedSet<LocalDate>.@@ - HorizontalCalendar( + // Precompute for fast membership checks + val doneDates = remember(statuses) { statuses.map { it.date }.toHashSet() } + + HorizontalCalendar( @@ - val done = statuses.any { it.date == day.date } + val done = day.date in doneDates @@ - val donePrevious = - statuses.any { it.date == day.date.minusDays(1) } - val doneAfter = - statuses.any { it.date == day.date.plusDays(1) } + val donePrevious = day.date.minusDays(1) in doneDates + val doneAfter = day.date.plusDays(1) in doneDates
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt(18 hunks)app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt(11 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (1)
app/src/main/java/com/shub39/grit/habits/presentation/util.kt (1)
formatDateWithOrdinal(92-98)
🔇 Additional comments (6)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (6)
24-30: LGTM: import reorg and UI kit refactor look consistent
- Moved core components to singular package and switched to ListItem and Rounded icon variants cleanly.
- Opt-ins for ExperimentalMaterial3/Expressive APIs are in place.
Also applies to: 40-44, 50-50, 60-60, 64-64, 77-80, 86-90
157-161: Top app bar nested scroll integration looks goodUsing
enterAlwaysScrollBehavior()and wiringnestedScroll(scrollBehavior.nestedScrollConnection)at the column level will propagate to the innerLazyColumn. Looks correct withMediumFlexibleTopAppBar.
248-285: LGTM: dialogs/bottom sheet migrationMigration to
GritDialog/GritBottomSheet, consistent iconography, input validation, and shape semantics all look solid. Good accessibility via content descriptions.Also applies to: 295-470
482-485: LGTM: gating WeekDayBreakdown viacanSeeContentUsing
canSeeContent = state.isUserSubscribedwith a plus action to trigger paywall is clear and self-contained.
700-707: Confirm paywall policy: Weekly heatmap isn’t gated, others are
WeeklyBooleanHeatMaplackscanSeeContentgating, whileWeekDayBreakdownandCalendarMapare gated. Verify this is intentional.If this should be gated, mirror the
AnalyticsCardusage:- AnalyticsCard( - title = stringResource(R.string.weekly_progress) - ) { + AnalyticsCard( + title = stringResource(R.string.weekly_progress), + canSeeContent = state.isUserSubscribed, + onPlusClick = { onAction(HabitsPageAction.OnShowPaywall) } + ) {
632-636: Padding/contentPadding updates: LGTMConsistent horizontal and bottom padding applied to charts/calendars improves visual rhythm and touch targets.
Also applies to: 705-707, 538-542
| modifier = Modifier.padding(bottom = 16.dp), | ||
| contentPadding = PaddingValues(horizontal = 16.dp), | ||
| monthHeader = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: today nullable in WeeklyBooleanHeatMap and used in comparisons
Same issue as CalendarMap. Make today non-null; otherwise comparisons and between-style calls won’t compile.
-private fun WeeklyBooleanHeatMap(
+private fun WeeklyBooleanHeatMap(
heatMapState: HeatMapCalendarState,
statuses: List<HabitStatus>,
- today: LocalDate?,
+ today: LocalDate,
onAction: (HabitsPageAction) -> Unit,
currentHabit: Habit,
primary: Color
) {Also consider the same Set optimization used in CalendarMap for done/neighbors:
@@
- HeatMapCalendar(
+ val doneDates = remember(statuses) { statuses.map { it.date }.toHashSet() }
+ HeatMapCalendar(
@@
- val done = statuses.any { it.date == day.date }
+ val done = day.date in doneDatesAlso applies to: 734-789, 693-699
| ListItem( | ||
| colors = ListItemDefaults.colors( | ||
| containerColor = MaterialTheme.colorScheme.primaryContainer, | ||
| leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, | ||
| headlineColor = MaterialTheme.colorScheme.onPrimaryContainer, | ||
| overlineColor = MaterialTheme.colorScheme.onPrimaryContainer, | ||
| supportingColor = MaterialTheme.colorScheme.onPrimaryContainer | ||
| ), | ||
| modifier = Modifier | ||
| .height(200.dp) | ||
| .fillMaxWidth(), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| horizontalArrangement = Arrangement.spacedBy(8.dp) | ||
| ) { | ||
| Card( | ||
| shape = MaterialTheme.shapes.extraLarge, | ||
| modifier = Modifier.weight(1f), | ||
| colors = CardDefaults.cardColors( | ||
| contentColor = MaterialTheme.colorScheme.onPrimaryContainer, | ||
| containerColor = MaterialTheme.colorScheme.primaryContainer | ||
| ) | ||
| ) { | ||
| Column( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .padding(16.dp), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| verticalArrangement = Arrangement.Center | ||
| ) { | ||
| Icon( | ||
| painter = painterResource(R.drawable.baseline_flag_circle_24), | ||
| contentDescription = "Flag", | ||
| modifier = Modifier.size(64.dp) | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(8.dp)) | ||
|
|
||
| Text( | ||
| text = stringResource(R.string.started_on), | ||
| style = MaterialTheme.typography.titleSmall | ||
| .clip( | ||
| RoundedCornerShape( | ||
| topStart = 30.dp, | ||
| topEnd = 30.dp, | ||
| bottomStart = 10.dp, | ||
| bottomEnd = 10.dp | ||
| ) | ||
|
|
||
| Text( | ||
| text = formatDateWithOrdinal(currentHabit.time.toLocalDate()), | ||
| fontWeight = FontWeight.Bold, | ||
| style = MaterialTheme.typography.bodyLarge | ||
| ) | ||
|
|
||
| Text( | ||
| text = stringResource( | ||
| R.string.days_ago_format, | ||
| ChronoUnit.DAYS.between(currentHabit.time.toLocalDate(), today) | ||
| ), | ||
| style = MaterialTheme.typography.bodyMedium | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| Card( | ||
| shape = MaterialTheme.shapes.extraLarge, | ||
| modifier = Modifier.weight(1f), | ||
| colors = CardDefaults.cardColors( | ||
| contentColor = MaterialTheme.colorScheme.onSecondaryContainer, | ||
| containerColor = MaterialTheme.colorScheme.secondaryContainer | ||
| ), | ||
| leadingContent = { | ||
| Icon( | ||
| imageVector = Icons.Rounded.FlagCircle, | ||
| contentDescription = "Flag", | ||
| modifier = Modifier.size(64.dp) | ||
| ) | ||
| ) { | ||
| Column( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .padding(16.dp), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| verticalArrangement = Arrangement.Center | ||
| ) { | ||
| Icon( | ||
| painter = painterResource(R.drawable.round_local_fire_department_24), | ||
| contentDescription = "Streak", | ||
| modifier = Modifier | ||
| .size(64.dp) | ||
| ) | ||
|
|
||
| Text( | ||
| text = stringResource(R.string.streak), | ||
| style = MaterialTheme.typography.titleSmall | ||
| ) | ||
| }, | ||
| overlineContent = { | ||
| Text( | ||
| text = stringResource(R.string.started_on), | ||
| ) | ||
| }, | ||
| headlineContent = { | ||
| Text( | ||
| text = formatDateWithOrdinal(currentHabit.time.toLocalDate()), | ||
| fontWeight = FontWeight.Bold, | ||
| ) | ||
| }, | ||
| supportingContent = { | ||
| Text( | ||
| text = stringResource( | ||
| R.string.days_ago_format, | ||
| ChronoUnit.DAYS.between(currentHabit.time.toLocalDate(), today) | ||
| ), | ||
| ) | ||
| } | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: today should be non-null in HabitStats (and is used as such)
ChronoUnit.DAYS.between and text color logic expect a non-null LocalDate. Change the parameter type.
-private fun HabitStats(
+private fun HabitStats(
currentHabit: Habit,
- today: LocalDate?,
+ today: LocalDate,
currentStreak: Int,
bestStreak: Int
) {The rest of the ListItem refactor looks good—clear hierarchy and appropriate container/foreground colors.
Also applies to: 848-889, 795-800
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt
around lines 795-800, 801-844 and 848-889 update HabitStats so the today
parameter is a non-null LocalDate (remove nullable type), adjust any callers to
pass a non-null LocalDate (derive from current context or default to
LocalDate.now() where appropriate), and update usages inside the component to
treat today as non-null (remove safe-calls or null checks used solely for
today). Ensure all ChronoUnit.DAYS.between and color logic receive the non-null
LocalDate and fix any compiler errors from changed signatures across the
codebase by updating call sites.
Signed-off-by: shub39 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (12)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (2)
108-111: Make icon decorative to avoid duplicate talkback announcementsThe button already has visible text. Setting a contentDescription on the icon causes screen readers to read both. Prefer
nullfor purely decorative icons.Apply:
- Icon( - imageVector = Icons.Rounded.Coffee, - contentDescription = "Buy me a coffee", - ) + Icon( + imageVector = Icons.Rounded.Coffee, + contentDescription = null, + )
125-129: Google Play icon: make decorative; optional size consistency
- Set
contentDescription = nullto avoid duplicate a11y label (text is present).- Nit: consider the default 24.dp for consistency with other icons.
Apply:
- Icon( - imageVector = FontAwesomeIcons.Brands.GooglePlay, - contentDescription = "Rate On Google Play", - modifier = Modifier.size(20.dp) - ) + Icon( + imageVector = FontAwesomeIcons.Brands.GooglePlay, + contentDescription = null, + modifier = Modifier.size(20.dp) + )Optional consistency tweak:
- modifier = Modifier.size(20.dp) + // modifier = Modifier.size(24.dp)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (10)
63-71: Consider exitUntilCollapsed for a denser settings list
enterAlwayskeeps the bar expanded and scrolls content under it;exitUntilCollapsedcollapses the large bar on scroll, giving more room to settings. UX call, but worth considering here.Apply:
- val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
96-99: Decorative leading icon in the “Grit Plus” cardMake icon decorative to avoid duplicated a11y announcements; the text “Grit Plus” already labels the control.
Apply:
- Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Grit Plus", - ) + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = null, + )
110-113: Unify arrow style and make it decorativeUse
Rounded.ArrowForwardIosto match the rest and setcontentDescription = null.Apply:
- Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "Grit Plus" - ) + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos, + contentDescription = null + )
255-260: Trailing chevron is decorativeListItem already conveys navigation; make the icon decorative.
Apply:
- trailingContent = { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos, - contentDescription = "Navigate" - ) - }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos, + contentDescription = null + ) + },
263-265: Leading “Palette” icon: decorativeApply:
- Icon( - imageVector = Icons.Rounded.Palette, - contentDescription = "Navigate", - ) + Icon( + imageVector = Icons.Rounded.Palette, + contentDescription = null, + )
283-288: Backup row chevron: decorativeApply:
- trailingContent = { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos, - contentDescription = "Navigate" - ) - }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos, + contentDescription = null + ) + },
291-293: Backup leading icon: decorativeApply:
- Icon( - imageVector = Icons.Rounded.Download, - contentDescription = "Backup", - ) + Icon( + imageVector = Icons.Rounded.Download, + contentDescription = null, + )
306-316: Supporting text duplicates the headlineThe supportingContent repeats “About libraries.” Either provide a description string or remove the block to reduce noise.
Two options:
- Remove the redundant supporting text (safe, no new resources):
- supportingContent = { - Text( - text = stringResource(R.string.about_libraries) - ) - },
- If you have/plan a description resource:
- supportingContent = { - Text( - text = stringResource(R.string.about_libraries) - ) - }, + supportingContent = { + Text( + text = stringResource(R.string.about_libraries_desc) + ) + },
319-321: About Libraries leading icon: decorativeApply:
- Icon( - imageVector = Icons.Rounded.Info, - contentDescription = "About Libraries" - ) + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = null + )
327-327: Spacer semantics nit
Spacer(modifier = Modifier.padding(60.dp))is unclear. Prefer explicit size.Apply:
- Spacer(modifier = Modifier.padding(60.dp)) + Spacer(modifier = Modifier.height(60.dp))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt(3 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt(4 hunks)app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt(7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt
🔇 Additional comments (3)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)
1-1: Package move to settings.ui.component looks consistent with the re-orgNo issues from a Compose or ABI standpoint. Call sites importing AboutApp should now target this package.
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (2)
1-1: Package rename to settings.ui.section is cleanNamespace matches the project’s new layout. No functional concerns.
15-20: Verify Compose Material3 and Icons dependencies
It appears your Gradle files do not declareandroidx.compose.material3:material3,androidx.compose.material:material-icons-extended, or a Compose BOM, so imports inRootPage.kt(lines 15–20) and usages ofLargeFlexibleTopAppBarand auto-mirrored icons at lines 25, 30, 35, 43, and 46 may fail to resolve.– In your module
build.gradle(.kts), ensure you have one of the following in thedependenciesblock:
•implementation("androidx.compose.material3:material3:<version>")
•implementation("androidx.compose.material:material-icons-extended:<version>")
• or the Compose BOM:
implementation(platform("androidx.compose:compose-bom:<version>"))• Nit: You mix
Filled.KeyboardArrowRightandRounded.ArrowForwardIos. For visual consistency, consider standardizing on theRoundedicon set.
| import androidx.compose.material.icons.Icons | ||
| import androidx.compose.material.icons.rounded.Coffee |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Switch to Material Icons for Coffee: confirm dependency is present
Using Icons.Rounded.Coffee requires material-icons-extended. Please verify the module includes that dependency (or BOM-managed alias), otherwise this will fail at compile.
Run this to confirm:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search Gradle files for material icons extended and material3 dependencies
rg -nP --glob '!**/build/**' --glob '!**/.gradle/**' \
'(androidx\.compose\.material:material-icons-extended|androidx\.compose\.material3:material3)' \
-- prettyLength of output: 226
Here’s a corrected sweep for any references to the extended icons or Material3 artifacts:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look in all Gradle files for the two dependencies
rg -nP --glob '*.gradle*' \
'(androidx\.compose\.material:material-icons-extended|androidx\.compose\.material3:material3)' -n .
# Check the version catalog if you’re using one
rg -nP --glob 'libs.versions.toml' \
'(material-icons-extended|material3)' -n .Length of output: 147
Add missing Material Icons Extended dependency
Our search found no references to androidx.compose.material:material-icons-extended or the Material3 BOM in your Gradle files or version catalog. Without this dependency, Icons.Rounded.Coffee will fail at compile time.
• In app/build.gradle (or the appropriate module’s Gradle file), add:
implementation "androidx.compose.material:material-icons-extended:<latest-version>"(or, if you’re using a BOM, include the Material3 or Compose BOM that brings in the extended icons)
• Alternatively, if you manage versions via libs.versions.toml, add an alias for material-icons-extended and reference it in your dependencies block.
Please ensure the correct version is aligned with your Compose libraries and re-run the build.
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
around lines 11-12, the code uses Icons.Rounded.Coffee but the project lacks the
Material Icons Extended dependency; add the appropriate dependency to the module
Gradle (e.g., implementation
"androidx.compose.material:material-icons-extended:<matching-version>") or add
an alias in libs.versions.toml and reference it, or include a Compose/Material3
BOM that brings in the extended icons; ensure the version you add matches your
Compose libraries and sync/rebuild.
Summary by CodeRabbit
New Features / UI
Bug Fixes
Chores
Removal