Skip to content

Conversation

@domonkosadam
Copy link
Contributor

refs: CLX-3127
affects: Horizon
release note: none

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR implements responsive layout improvements for the dashboard widgets, adding support for wide layouts (tablets/larger screens). Overall, this is a well-structured refactoring that improves the user experience on larger devices.

Strengths

Excellent responsive design patterns - The use of BoxWithConstraints with isWideLayout extension provides a clean, reusable approach to responsive layouts

Code organization - Good extraction of logic into separate composables (NumericWidgetRow, DashboardCourseCardWideContent, etc.)

Consistent loading states - The introduction of companion object Loading states (e.g., DashboardCourseCardState.Loading) provides consistency across widgets

Improved visual feedback - Better use of shimmer effects throughout loading states

Code cleanup - Removed unused code (e.g., DashboardCourseCardLoading.kt, unused button state logic)

Areas for Consideration

🔍 Magic numbers - The 400.dp breakpoint and 320.dp image width should be extracted as design tokens/constants for easier maintenance

🔍 Performance - The beyondViewportPageCount = 3 in AnimatedHorizontalPager preloads all pages, which may impact performance

🔍 Localization - Verify that hardcoded minimum widths and placeholder text work well across all supported locales

🔍 Empty else branch - Line 353 in DashboardScreen.kt has an empty else that can be cleaned up

Testing Recommendations

  • Test on various device sizes, especially foldables and tablets in different orientations
  • Verify loading state transitions work smoothly
  • Check accessibility with TalkBack on both compact and wide layouts
  • Validate RTL layout support for the new wide layouts

Great work on improving the dashboard experience for larger screens! 🚀

Modifier.width(IntrinsicSize.Max)
)
DashboardSkillOverviewWidget(
homeNavController,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good refactoring to extract this logic into a separate composable! This improves code organization and makes the different layout strategies clearer.

Consider adding a @Preview annotation for this composable to make it easier to test both layout variations visually during development.

.fillMaxWidth()
.padding(bottom = 16.dp)
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The beyondViewportPageCount = 3 parameter preloads all 3 pages, which could impact performance if these widgets load heavy data or complex layouts. Consider whether this is necessary, or if a lower value (e.g., 1) would suffice while still providing smooth scrolling.

.padding(bottom = 16.dp)
)
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty else branch can be removed for cleaner code. If this is intentional for future expansion, consider adding a comment explaining why it's here.

val isLoading: Boolean = false,
)
) {
companion object {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice pattern using a companion object for loading states! This makes it easy to provide consistent loading UI across the app. Consider if these placeholder values should be localized or if they're intentionally non-localized as placeholder text.

))
} else state
}
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the button state update logic simplifies the ViewModel significantly. However, I'm curious - was this functionality removed intentionally, or has it been moved elsewhere? If the button loading states are no longer needed, that's great. If they are needed, we should ensure they're handled appropriately.


@Composable
private fun ProgramsText(
programs: List<DashboardCourseCardParentProgramState>,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good improvement adding shimmerEffect to all the loading states! This provides better visual feedback. The use of isLoading parameter threading through all these components is clean.

One minor suggestion: Consider extracting the shimmer colors (especially for the institution-themed ones) into constants or the design system to ensure consistency.

modifier = Modifier
.fillMaxWidth()
modifier = modifier
.aspectRatio(1.69f)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixing isImageLoading and isLoading in the shimmer condition might lead to loading states persisting longer than intended. Consider whether both need to gate the shimmer effect, or if just isImageLoading is sufficient for the image component.

}
}

val BoxWithConstraintsScope.isWideLayout
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 400.dp threshold for wide layout is a reasonable choice, but consider:

  1. Design consistency: Ensure this threshold aligns with your design system's breakpoint definitions
  2. Testing: Has this been tested across different device types (phones, tablets, foldables)?
  3. Naming: Consider if isWideLayout accurately describes what this represents - perhaps isTabletLayout or shouldUseWideLayout might be clearer?

Also, this is a great candidate for extracting into a design token constant rather than a magic number.

)

BoxWithConstraints {
if (this.isWideLayout) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great use of responsive layout here! The switch between Row and Column based on layout width is clean.

Note: In the Row layout, the skills use Modifier.weight(1f) which is good, but if the number of skills varies, you might want to handle cases where there are too many skills to fit comfortably in a row (e.g., by limiting display or using a scrollable row).

modifier = Modifier
.shimmerEffect(isLoading)
.focusable()
.widthIn(min = 100.dp)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition of widthIn(min = 100.dp) to prevent the time display from being too narrow. However, verify this works well with localized content in languages with longer number formats or different time representations.

@github-actions
Copy link

github-actions bot commented Nov 3, 2025

📊 Code Coverage Report

✅ Student

  • PR Coverage: 44.00%
  • Master Coverage: 44.00%
  • Delta: +0.00%

✅ Teacher

  • PR Coverage: 25.58%
  • Master Coverage: 25.58%
  • Delta: +0.00%

⚠️ Pandautils

  • PR Coverage: 22.49%
  • Master Coverage: 22.49%
  • Delta: -0.00%

📈 Overall Average

  • PR Coverage: 30.69%
  • Master Coverage: 30.69%
  • Delta: -0.00%

@inst-danger
Copy link
Contributor

Student Install Page

companion object {
val Loading = DashboardPaginatedWidgetCardItemState(
chipState = DashboardPaginatedWidgetCardChipState(
label = "Announcement",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these labels visible while loading?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, these are just placeholders to get the correct size. The skeleton loading indicator will be drawn instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants