6.5 KiB
core:ui
Material 3 theme and shared Compose components used across all feature modules. Purely presentational -- no business logic, no ViewModels, no repositories.
What This Module Provides
Theme (theme/)
Theme.kt:LibreChatThemewrapping Material 3MaterialTheme. Maps LibreChat web colors to M3 color roles.Color.kt: Color palette derived from the web app's CSS variables.Type.kt: Typography scale.Shape.kt: Corner radius definitions.
Shared Components (components/)
LibreChatTopBar- App bar with optional back navigation and actions.LoadingIndicator- Centered circular progress.ErrorBanner- Dismissible error message bar.EmptyState- Illustration + message for empty lists.AvatarImage- User/agent/model avatar with Coil image loading and fallback.ConfirmationDialog- Reusable confirm/cancel dialog.ModelIcon- Endpoint-specific model icons.EndpointBadge- Colored badge showing the AI provider.SearchBar- Reusable search input field.BottomSheetScaffold- Wrapper for modal bottom sheets.PullToRefresh- Pull-to-refresh wrapper.BannerDisplay- Dismissible server banner cards (info/warning/error types).
Markdown (markdown/)
MarkdownRenderer- Compose-native markdown rendering (commonmark).CodeBlock- Syntax-highlighted code blocks (Compose-native, NOT WebView per block).LatexRenderer- Math rendering via WebView or compose-math.
Message Components (message/)
MessageBubble- Shared message rendering used by:feature:chat.ToolCallCard- Expandable tool call display.FileAttachmentChip- File reference chip.FeedbackButtons- Thumbs up/down.StreamingIndicator- Pulsing indicator during SSE streaming.
Rules
- No business logic. No ViewModels, no repository calls, no use cases.
- Components accept domain models from
:core:modelas parameters and render them. - All components must be stateless or hoist state to the caller.
- Dependencies:
:core:model,:core:common, Coil for image loading, Compose libraries. - Convention plugins:
librechat.mobile.library+librechat.mobile.compose. - No DI in this module (no Koin modules, no injected classes).
- Use
@Previewannotations on all components for Android Studio preview support. - During SSE streaming, buffer markdown re-renders to ~100ms intervals to avoid frame drops.
Dynamic Parameters (components/Dynamic*.kt)
DynamicParameterPaneldispatchesList<ParameterDefinition>to typed controls (slider/dropdown/checkbox/input/textarea)ParameterDefinitioncontains: key, type, label, description, default, min, max, step, optionsDynamicSlidersnaps to step increments; calculates stepCount from range- All controls are stateless — caller manages values via
Map<String, String> ModelParameterSheetfalls back to existing fixed params when no schema available- Gotcha: Slider step count calculated as
((max - min) / step).toInt()— ensure step divides range evenly to avoid drift
Accessibility (theme/AccessibilityUtils.kt)
Modifier.minTouchTarget()— enforces 48dp minimum sizeModifier.semanticHeading()— marks composable as heading for TalkBack- Helper functions:
endpointContentDescription(),modelIconContentDescription() - Applied to
LoadingIndicatorandEmptyStatecomponents
Compose Performance Rules
These rules apply to ALL composables across feature modules, not just core:ui.
UI State Architecture
-
Single UI state data class per screen. Each ViewModel exposes ONE
StateFlow<XyzUiState>— never 5+ individual StateFlows that each trigger recomposition independently. -
UI state contains only display-ready data. The ViewModel maps domain models (e.g.,
Conversationwith 28 fields) to minimal display data classes (e.g.,DrawerConversationDisplayDatawith 7 fields). Composables should never receive full domain models. -
Mark UI state classes
@Immutable. This lets the Compose compiler skip recomposition when the reference hasn't changed. All fields must bevalwith stable types. -
Collect state at the narrowest scope. If only
DrawerContentuses drawer state, collect insideDrawerContent— not in the parentPhoneLayoutwhich also owns the NavDisplay. State changes should only recompose the composable that reads them. -
Use
SharingStarted.Eagerlyfor UI state that should be ready before the composable subscribes (avoids empty→populated two-phase render jank on first frame).
Stability
-
All types passed to composables must be stable. Primitives,
String,@Immutabledata classes, and enums are stable. StandardList/Set/Mapare NOT stable by default — they are declared stable incompose-stability.confat the project root. -
Never pass lambdas that capture unstable references. Prefer method references (
viewModel::doThing) orremember'd lambdas over inline lambdas that capture changing state.
LazyColumn / LazyList
-
Always provide
keyonitems()calls. Keys must be unique, stable identifiers (e.g.,conversationId). -
Always provide
contentTypewhen a LazyColumn has mixed item types (headers, content rows, loading indicators). This enables Compose to reuse compositions across items of the same type. -
One element per
item {}block. Multiple composables in one block prevents independent recycling. -
No nested same-direction scrollables. Never put
LazyColumninsideverticalScroll— combine into oneLazyColumnusingitem {}blocks.
Avoiding Unnecessary Work
-
Move data transformations to the ViewModel. Sorting, filtering, grouping, and model mapping happen in the ViewModel — not in
rememberblocks in composables. -
Cache expensive objects as top-level constants.
RoundedCornerShape,Regex,DateTimeFormatter— allocating these per-item per-frame is wasteful. -
Use
background(color, shape)instead ofclip(shape).background(color). Theclipmodifier creates a persistent clip layer per composable;backgroundwith a shape parameter draws the clipped background without the layer overhead. -
Avoid
copy(alpha = ...)on colors. Alpha-blended colors force GPU compositing. Use opaque colors from the theme where possible. -
Defer state reads to later phases. Use lambda-based modifiers (
Modifier.offset { },Modifier.drawBehind { }) instead of value-based ones (Modifier.offset(x, y),Modifier.background(animatedColor)) for scroll/animation state.