Skip to content

feat(android): Material Design UI#26821

Closed
akshaynexus wants to merge 22 commits intoopenclaw:mainfrom
akshaynexus:redesign-android-material
Closed

feat(android): Material Design UI#26821
akshaynexus wants to merge 22 commits intoopenclaw:mainfrom
akshaynexus:redesign-android-material

Conversation

@akshaynexus
Copy link
Copy Markdown

@akshaynexus akshaynexus commented Feb 25, 2026

Pull Request: Android App Redesign (Material Design 3)

Note

AI-Assisted PR: This pull request description and parts of the implementation logic were prepared with the assistance of Gemini 3 Flash, Minimax M2.5


Summary

  • UI was inconsistent and didn't align with modern Android design patterns
  • Migrated to Material Design 3: updated color palettes (Dynamic Color/Material You), standardized typography, and M3 components throughout
  • No changes to backend, data layer, or business logic

Change Type

  • Feature
  • Refactor

Scope

  • UI / DX

Changes

  • Complete visual overhaul with M3 look and feel
  • Proper light/dark color schemes with Dynamic Color support
  • Improved accessibility: better contrast ratios, larger touch targets
  • Updated animations and ripple effects

Security Impact

No new permissions, secrets, network calls, or data access changes.

Repro + Verification

Environment: Android 13/14, Pixel 7 Emulator

Steps:

  1. Build and launch on Android device/emulator
  2. Navigate through all primary fragments/activities
  3. Toggle system-wide Dark Mode
  4. Check layout on different screen densities

Expected: All UI elements follow M3 guidelines, Dynamic Color adjusts on supported devices.

Actual: UI consistent with M3; no visual regressions found.

Screenshots

Human Verification

  • Tested primary user flow (Login → Dashboard → Settings) on physical Android device
  • Dark mode toggle confirmed working on-device
  • Text scaling at 150% and small-screen layouts checked
  • Foldable/ChromeOS not tested

Compatibility

  • Backward compatible: Yes
  • Uses MaterialComponents compatibility library for graceful fallback on legacy devices

Revert

Revert this PR and restore themes.xml, colors.xml, and layout resource files. Watch for color contrast issues on older API levels or missing icons in navigation states.


Greptile Summary

Successfully migrated Android UI from legacy components to Material Design 3, implementing dynamic color support (Material You), updated color palettes for light/dark themes, and modern M3-compliant components throughout the app.

  • Replaced custom theme colors (mobileAccent, mobileText, etc.) with MaterialTheme.colorScheme properties
  • Updated all composables to use M3 components: rounded corners (18dp bubbles, 22dp composer), proper elevation, and semantic color tokens
  • Implemented proper light/dark color schemes with fallback support for devices below API 31
  • Enhanced accessibility with proper contrast ratios and larger touch targets
  • Removed role labels from chat bubbles, streamlined UI to focus on content
  • Updated thinking level UI, settings sheet, and onboarding flow with M3 design patterns

Confidence Score: 4/5

  • Safe to merge with one minor fix needed for code block theming
  • Check ChatMessageViews.kt:351 — hardcoded border color needs theme integration for proper dark mode support

Last reviewed commit: 10eabbc

Migrate chat UI to MaterialTheme values and remove the session connection pill. Replaced legacy color tokens (mobileAccent, mobileWarning, mobileBorder, etc.) and mobile typography with MaterialTheme.colorScheme and MaterialTheme.typography equivalents; updated image Surface border and background to use outlineVariant and surfaceContainerHighest. Removed the healthOk parameter and the ChatConnectionPill composable from ChatSheetContent/ChatThreadSelector and simplified the session label layout accordingly.
Large UI refactor across chat components to modernize and polish the chat experience. Key changes:

- ChatComposer: rebuilt input area using BasicTextField, added IME handling (Send action), imePadding, adaptive send/stop/refresh/mic buttons, thinking control block, improved attachments strip placement, and visual refinements (rounded surface, subtle borders, progress indicator).
- ChatMessageListCard: added background tint, safer scroll-to-top check, adjusted paddings/spacing and improved empty-chat hint styling for light/dark modes.
- ChatMessageViews: simplified and unified message bubble styling (new rounded shapes, spacing, and text color use), removed legacy mobile theme tokens, improved typing indicator, pending tools layout, base64 image display scaling, dot pulse and code block styling.
- ChatSheetContent & thread selector: use imePadding, layout spacing and chip styling tweaks, simplified error rail.

Also removed/merged several small helper functions and replaced custom tokens with MaterialTheme usages to improve consistency and maintainability.
@akshaynexus akshaynexus marked this pull request as ready for review February 25, 2026 20:06
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Adjust ChatComposer right-side controls to show Stop while processing and a combined Refresh + Mic group when the input is empty (instead of separate states). Tweak icon sizes, spacing and colors (mic uses primary tint; refresh reduced to 20dp) to improve layout and affordance.

Replace the static PulseDot typing indicator in ChatMessageViews with an AnimatedDots implementation using rememberInfiniteTransition and staggered animateFloat tweens (reverse repeat). Add padding/spacing and required animation imports to provide a Material 3-style animated typing indicator.
Introduce a user-configurable theme mode and switch UI components to use MaterialTheme. Adds SecurePrefsThemeMode enum and persistence (SecurePrefs), exposes a themeMode StateFlow and setter through NodeRuntime and MainViewModel, and MainActivity collects and passes the mode into OpenClawTheme. OpenClawTheme now resolves dark/light based on the selected mode. SettingsSheet gains an Appearance section with radio options (System/Light/Dark). Several UI files (OnboardingFlow, ChatMarkdown, VoiceTabScreen, SettingsSheet) were updated to use MaterialTheme colors/typography and simplified styles/typography, and minor layout/label cleanups were made.
Remove the static "Session" header and the current session label (and its computed currentSessionLabel/friendlySessionName) from ChatThreadSelector. Cleans up an unused variable and simplifies the selector UI while keeping the session options row intact.
Remove the title content from the post-onboarding top app bar and enhance the VoiceTabScreen UX: add tactile haptic feedback when toggling the mic (performHapticFeedback handling API levels and errors), smooth audio level animation (animateFloatAsState), increase and densify waveform bars, adjust animation timing/spacing, and update color thresholds for audio intensity. Also minor imports and layout tweaks to support the changes. These changes improve visual responsiveness and provide tactile confirmation for mic actions.
Break out SettingsSheet into reusable composable sections (SettingsHeader, SectionCard, AppearanceSection, NodeSection, VoiceSection, CameraSection, MessagingSection, LocationSection, ScreenSection, DebugSection) and small building blocks (ThemeOption, PermissionCard, ToggleRow, InfoRow, LocationOption). Replace many ListItem blocks with Material3 cards, icons and compact rows, simplify layout/spacing and typography, and centralize openAppSettings. Also improve VoiceTabScreen mic status rendering: detect error states and show an error-styled surface for error messages (or muted text for normal status) instead of the previous button. Overall UI/UX and code organization improvements for settings and voice status handling.
Pass the activeTab into the top status bar and render its label as the CenterAlignedTopAppBar title. Remove the redundant SettingsHeader composable and its list item from SettingsSheet. Simplify VoiceTabScreen layout by removing the header row (including the Connected badge), the explanatory empty-state text and micStatusText display; adjust LazyColumn modifiers and spacing to match the streamlined layout. Minor import adjustments to support the layout changes.
Replace the horizontally scrollable row of FilterChips with a single FilterChip that opens a DropdownMenu. The chip shows the current session and toggles expand/collapse; the menu lists sessions, highlights the active one with a check icon and stronger typography, and closes on selection. Added state and UI imports to support the dropdown and adjusted styling/colors for selected state to improve usability and reduce horizontal scrolling.
@akshaynexus
Copy link
Copy Markdown
Author

Done

@akshaynexus
Copy link
Copy Markdown
Author

Hey, just wanted to bump this in case it got buried — happy to address any feedback or make changes if needed

@akshaynexus akshaynexus closed this Mar 3, 2026
@akshaynexus akshaynexus reopened this Mar 8, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR successfully migrates the Android app from custom legacy color tokens (mobileAccent, mobileText, etc.) to a full Material Design 3 implementation with dynamic color (Material You) support, proper light/dark color schemes, a user-controlled theme mode picker, and modern M3 components throughout (NavigationBar, ElevatedCard, LargeFloatingActionButton, FilterChip, etc.). The breadth of the migration is impressive and most files are cleanly converted.

Issues found:

  • ChatMessageListCard.kt (lines 35–37, 98–99): The chat message area background and empty-state hint colors are hardcoded Telegram-style hex values (0xFF17212B / 0xFFEFEFF3) derived from a manual luminance check. These won't respond to dynamic color palettes or themeMode overrides — directly contradicting the goal of this PR. Use MaterialTheme.colorScheme.surfaceContainerLowest and onSurfaceVariant tokens instead.
  • ChatMarkdown.kt (lines 85–86): File-level codeBlockBg and codeBlockText constants are hardcoded dark-mode colors that won't adapt in light theme. These should be resolved at composition time from MaterialTheme.colorScheme.
  • ChatComposer.kt (lines 179–201): The Refresh and Mic IconButtons shown when the input field is empty have onClick = { } — they are fully rendered and appear interactive but silently do nothing. The Refresh button should invoke onRefresh; the Mic button should be disabled or gated until the navigation hook is wired.
  • OpenClawTheme.kt (line 112): overlayContainerColor() still branches on isSystemInDarkTheme() rather than the resolved darkTheme state. Users who force Dark mode while the OS is in Light mode will see incorrect overlay container colors.
  • VoiceTabScreen.kt (lines 46–47): Duplicate import androidx.compose.foundation.lazy.items will cause a compiler warning.

Confidence Score: 3/5

  • Not safe to merge as-is — two non-functional UI elements and hardcoded colors that undermine the primary goal of the PR need to be addressed first.
  • The overall M3 migration is thorough and well-executed across most files, but there are two functional regressions (non-functional Refresh/Mic buttons, overlay color logic ignoring user theme mode) and two places where hardcoded colors directly contradict the dynamic theming goal (chat background, code block colors). These are real issues that affect correctness and user experience, not just style nits.
  • ChatMessageListCard.kt (hardcoded background colors), ChatComposer.kt (non-functional buttons), ChatMarkdown.kt (hardcoded code-block colors), OpenClawTheme.kt (overlayContainerColor theme mode mismatch)

Last reviewed commit: bf58c75

Comment on lines +35 to +37
val surfaceColor = MaterialTheme.colorScheme.surface
val isDark = surfaceColor.red * 0.299 + surfaceColor.green * 0.587 + surfaceColor.blue * 0.114 < 0.5
val bgColor = if (isDark) Color(0xFF17212B) else Color(0xFFEFEFF3)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hardcoded colors bypass dynamic theming

The chat list background is computed from a luminance check and then mapped to fixed hex values (0xFF17212B for dark, 0xFFEFEFF3 for light). These are Telegram-inspired colors that won't respond to Material You dynamic color palettes, which is the core goal of this PR.

MaterialTheme.colorScheme.surface (or surfaceContainerLowest) should be used directly instead — it already tracks the user's chosen theme mode and dynamic color seed.

Suggested change
val surfaceColor = MaterialTheme.colorScheme.surface
val isDark = surfaceColor.red * 0.299 + surfaceColor.green * 0.587 + surfaceColor.blue * 0.114 < 0.5
val bgColor = if (isDark) Color(0xFF17212B) else Color(0xFFEFEFF3)
val bgColor = MaterialTheme.colorScheme.surfaceContainerLowest

The isDark derived variable and the same pair of hardcoded colors in EmptyChatHint (lines 98–99) should be updated in the same way.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageListCard.kt
Line: 35-37

Comment:
**Hardcoded colors bypass dynamic theming**

The chat list background is computed from a luminance check and then mapped to fixed hex values (`0xFF17212B` for dark, `0xFFEFEFF3` for light). These are Telegram-inspired colors that won't respond to Material You dynamic color palettes, which is the core goal of this PR.

`MaterialTheme.colorScheme.surface` (or `surfaceContainerLowest`) should be used directly instead — it already tracks the user's chosen theme mode and dynamic color seed.

```suggestion
  val bgColor = MaterialTheme.colorScheme.surfaceContainerLowest
```

The `isDark` derived variable and the same pair of hardcoded colors in `EmptyChatHint` (lines 98–99) should be updated in the same way.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +85 to +86
private val codeBlockBg = Color(0xFF15171B)
private val codeBlockText = Color(0xFFE8EAEE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Static code-block colors ignore light theme

codeBlockBg (0xFF15171B) and codeBlockText (0xFFE8EAEE) are dark-mode-only values extracted from the old mobileCodeBg/mobileCodeText tokens. In light mode these will still produce a dark background with near-white text — visually tolerable for code, but inconsistent with the rest of the M3 migration and will look wrong on light-themed devices.

These should be @Composable values drawn from the theme. For example:

// Replace file-level constants with composable usage at call site
val inlineStyles = InlineStyles(
    inlineCodeBg = MaterialTheme.colorScheme.surfaceContainerHighest,
    inlineCodeColor = MaterialTheme.colorScheme.onSurface,
    linkColor = MaterialTheme.colorScheme.primary,
)

(and thread the code-block colors similarly through ChatCodeBlock)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMarkdown.kt
Line: 85-86

Comment:
**Static code-block colors ignore light theme**

`codeBlockBg` (`0xFF15171B`) and `codeBlockText` (`0xFFE8EAEE`) are dark-mode-only values extracted from the old `mobileCodeBg`/`mobileCodeText` tokens. In light mode these will still produce a dark background with near-white text — visually tolerable for code, but inconsistent with the rest of the M3 migration and will look wrong on light-themed devices.

These should be `@Composable` values drawn from the theme. For example:
```kotlin
// Replace file-level constants with composable usage at call site
val inlineStyles = InlineStyles(
    inlineCodeBg = MaterialTheme.colorScheme.surfaceContainerHighest,
    inlineCodeColor = MaterialTheme.colorScheme.onSurface,
    linkColor = MaterialTheme.colorScheme.primary,
)
```
(and thread the code-block colors similarly through `ChatCodeBlock`)

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +179 to +201
IconButton(
onClick = { },
modifier = Modifier.size(36.dp),
) {
Icon(
Icons.Default.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp),
)
}
IconButton(
onClick = { },
modifier = Modifier.size(36.dp),
) {
Icon(
Icons.Default.Mic,
contentDescription = "Voice",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(22.dp),
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mic and Refresh buttons are non-functional

Both IconButtons in the "empty input" state have onClick = { } — they render as fully interactive controls (correct tint, correct size, accessible content descriptions) but do nothing when tapped. Users who tap either button will get no feedback or action.

  • The Refresh button should call onRefresh (which is already a parameter of ChatComposer).
  • The Mic button presumably needs a new callback to navigate to or activate the Voice tab; if it isn't wired yet it should at minimum be enabled = false or replaced with a placeholder until the navigation hook is implemented.
Suggested change
IconButton(
onClick = { },
modifier = Modifier.size(36.dp),
) {
Icon(
Icons.Default.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp),
)
}
IconButton(
onClick = { },
modifier = Modifier.size(36.dp),
) {
Icon(
Icons.Default.Mic,
contentDescription = "Voice",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(22.dp),
)
}
}
IconButton(
onClick = onRefresh,
modifier = Modifier.size(36.dp),
) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt
Line: 179-201

Comment:
**Mic and Refresh buttons are non-functional**

Both `IconButton`s in the "empty input" state have `onClick = { }` — they render as fully interactive controls (correct tint, correct size, accessible content descriptions) but do nothing when tapped. Users who tap either button will get no feedback or action.

- The **Refresh** button should call `onRefresh` (which is already a parameter of `ChatComposer`).
- The **Mic** button presumably needs a new callback to navigate to or activate the Voice tab; if it isn't wired yet it should at minimum be `enabled = false` or replaced with a placeholder until the navigation hook is implemented.

```suggestion
              IconButton(
                onClick = onRefresh,
                modifier = Modifier.size(36.dp),
              ) {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +112 to +116
return if (isSystemInDarkTheme()) {
scheme.surfaceContainerLow.copy(alpha = 0.92f)
} else {
scheme.surfaceContainerHigh.copy(alpha = 0.88f)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

overlayContainerColor ignores user-set theme mode

overlayContainerColor() branches on isSystemInDarkTheme(), but after this PR the app can be forced into dark or light mode independently of the OS setting via SecurePrefsThemeMode. If a user selects "Force Dark" while the OS is in light mode, isSystemInDarkTheme() returns false, so the overlay will use the light treatment even though the rendered color scheme is dark — causing a visual mismatch.

The darkTheme decision that OpenClawTheme already computes should drive this helper instead. One approach is to expose darkTheme through LocalDarkTheme or simply check the current color scheme's lightness:

fun overlayContainerColor(): Color {
  val scheme = MaterialTheme.colorScheme
  val isDark = scheme.surface.luminance() < 0.5f
  return if (isDark) {
    scheme.surfaceContainerLow.copy(alpha = 0.92f)
  } else {
    scheme.surfaceContainerHigh.copy(alpha = 0.88f)
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/OpenClawTheme.kt
Line: 112-116

Comment:
**`overlayContainerColor` ignores user-set theme mode**

`overlayContainerColor()` branches on `isSystemInDarkTheme()`, but after this PR the app can be forced into dark or light mode independently of the OS setting via `SecurePrefsThemeMode`. If a user selects "Force Dark" while the OS is in light mode, `isSystemInDarkTheme()` returns `false`, so the overlay will use the light treatment even though the rendered color scheme is dark — causing a visual mismatch.

The `darkTheme` decision that `OpenClawTheme` already computes should drive this helper instead. One approach is to expose `darkTheme` through `LocalDarkTheme` or simply check the current color scheme's lightness:

```kotlin
fun overlayContainerColor(): Color {
  val scheme = MaterialTheme.colorScheme
  val isDark = scheme.surface.luminance() < 0.5f
  return if (isDark) {
    scheme.surfaceContainerLow.copy(alpha = 0.92f)
  } else {
    scheme.surfaceContainerHigh.copy(alpha = 0.88f)
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 46 to +47
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.items
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Duplicate import

androidx.compose.foundation.lazy.items is imported twice. Kotlin will emit a warning and some configurations treat duplicate imports as errors. Remove the duplicate.

Suggested change
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.items
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/VoiceTabScreen.kt
Line: 46-47

Comment:
**Duplicate import**

`androidx.compose.foundation.lazy.items` is imported twice. Kotlin will emit a warning and some configurations treat duplicate imports as errors. Remove the duplicate.

```suggestion
import androidx.compose.foundation.lazy.items
```

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 30, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

@openclaw-barnacle openclaw-barnacle bot closed this Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: android App: android size: XL stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant