tahamajs's picture
|
download
raw
18.7 kB
The UI of the Air Mouse Android application is a **modern, reactive, and highly modular system** built entirely with Jetpack Compose. It follows a **Unidirectional Data Flow (UDF)** architecture, meaning the UI is a pure function of state, and user interactions trigger events that update that state.
Here is a **complete, end‑to‑end explanation** of how the UI works, from the moment the app launches to the deepest gesture interaction.
---
## 1. The Big Picture: Layers of the UI
The UI system is divided into four interconnected layers:
1. **Entry & Navigation** – How the app starts and moves between screens.
2. **Theming & Styling** – How colours, shapes, and typography are dynamically applied.
3. **Screen Architecture (MVI)** – The internal structure of each screen (State, Events, ViewModel).
4. **Component Library** – The reusable building blocks that ensure consistency.
---
## 2. Entry Point & Root Container
### `MainActivity` – The Launchpad
- **Splash Screen**: Uses `installSplashScreen()` and keeps it visible while the app loads resources and checks permissions.
- **Onboarding Check**: If `prefs.isOnboardingCompleted()` is false, it immediately launches `OnboardingActivity` and finishes. Otherwise, it proceeds.
- **Permission Handling**: Checks for Camera, Bluetooth, Microphone, and Notifications permissions. Shows a rationale screen if they are missing, and retries.
- **Theme Application**: Reads `theme` and `accent_color` from preferences and applies them via `AirMouseTheme`.
- **Sensor Initialisation**: Starts the `SensorService` and sets the `onOrientationChanged` callback to send movement data via the `ConnectionManager`.
- **Set Content**: Finally, it calls `setContent { MainScreen() }`, rendering the root UI.
### `MainScreen.kt` – The Root UI Container
This is the heart of the UI. It sets up the `NavHostController`, `DrawerState`, and the top-level `Scaffold`.
```kotlin
@Composable
fun MainScreen(...) {
val navController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = { ModernDrawerContent(...) }
) {
Scaffold(
topBar = { /* Top App Bar with menu, title, status chip */ },
bottomBar = { if (shouldShowBottomBar(currentRoute)) ModernBottomBar(...) },
floatingActionButton = { /* Contextual FAB (Calibrate, Touchpad, etc.) */ }
) { paddingValues ->
MainNavHost(
navController = navController,
modifier = Modifier.padding(paddingValues),
onOpenDrawer = { scope.launch { drawerState.open() } }
)
}
}
}
```
**Key components inside `MainScreen`**:
- **`ModalNavigationDrawer`**: The side menu (drawer) that appears when the hamburger icon is clicked.
- **`TopAppBar`**: Displays the app title and connection status (e.g., "Online" or "Waiting for approval").
- **`ModernBottomBar`**: Shows 4 tabs: **Home**, **Statistics**, **Settings**, **Help**. It only appears when the current route is one of these four (controlled by `shouldShowBottomBar`).
- **`FloatingActionButton`**: Dynamically changes based on the current route and control mode (e.g., shows "Calibrate" on Home, "Touchpad" when in touchpad mode).
- **`MainNavHost`**: The actual navigation graph where all screens are defined.
---
## 3. Navigation System
### `Destinations.kt` – The Route Registry
Every screen is defined as a `sealed class Destinations` with a unique `route`, a `title`, and an `icon`. This centralises all routing logic.
```kotlin
sealed class Destinations(val route: String, val title: String, val icon: ImageVector) {
object Home : Destinations("home", "Home", Icons.Default.Home)
object Settings : Destinations("settings", "Settings", Icons.Default.Settings)
// ... over 20 destinations
}
```
### `NavigationActions.kt` – Abstraction for Navigation
Instead of calling `navController.navigate()` directly, the UI uses the `NavigationActions` interface. This makes the UI testable and decouples it from the specific `NavController` implementation.
```kotlin
interface NavigationActions {
fun navigateTo(route: String)
fun navigateBack()
fun navigateToHome()
fun navigateToSettings()
// ... etc.
}
```
### `MainNavHost.kt` – The NavGraph
This function defines the `NavHost` and maps each `composable` route to its respective screen composable.
- **Transitions**: Uses `enterTransition` and `exitTransition` with `fadeIn` and `slideInHorizontally` for smooth screen changes.
- **Arguments**: Supports passing arguments (e.g., `quality` for the calibration result screen).
- **Dependency Injection**: Each screen uses `hiltViewModel()` to get its ViewModel.
**How navigation works:**
1. A user clicks a button in `HomeScreen`.
2. The button calls `navigationActions.navigateTo(Destinations.Settings.route)`.
3. `NavigationActionsImpl` calls `navController.navigate(route)`.
4. The `NavHost` matches the route and renders the `SettingsScreen` composable.
5. The back button is handled by `navigationActions.navigateBack()`.
---
## 4. Theming & Styling – Dynamic and Centralised
### `Theme.kt` & `ThemeColors.kt` – The Colour System
The UI supports **light, dark, pure black, high contrast**, and several **premium themes** (Ocean, Sunset, Forest, etc.), all with customisable **accent colours**.
- **`ThemeColorScheme`**: A data class containing all Material 3 colour roles (`background`, `surface`, `primary`, `onSurface`, etc.).
- **`ThemeColorSchemes`**: An object that provides predefined schemes (e.g., `darkTheme()`, `oceanTheme()`).
- **`AccentColor`**: An enum with 14 colours (Orange, Blue, Green, Purple, Pink, etc.), each with its own hex codes.
### `LocalThemeColors` – CompositionLocal
To avoid passing colours down through every composable, the theme is provided via a `CompositionLocal`.
```kotlin
val LocalThemeColors = staticCompositionLocalOf { ThemeColorSchemes.darkTheme(AccentColor.ORANGE) }
@Composable
fun ProvideThemeColors(colors: ThemeColorScheme, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalThemeColors provides colors) {
content()
}
}
```
Any composable can access the current theme colours with `val colors = LocalThemeColors.current`.
### `AirMouseTheme` – The Wrapper
This composable reads the current `themeId` and `accentColor` from preferences, generates the `ThemeColorScheme`, and wraps the entire app content.
```kotlin
@Composable
fun AirMouseTheme(themeId: String, accentColor: AccentColor, content: @Composable () -> Unit) {
val colors = getThemeColorScheme(themeId, accentColor)
ProvideThemeColors(colors) {
MaterialTheme(
colorScheme = // Maps ThemeColorScheme to M3 ColorScheme,
typography = AirMouseTypography,
shapes = AirMouseShapes,
content = content
)
}
}
```
### `Dimensions.kt` & `Shapes.kt`
- **`Dimensions`**: Centralises all spacing (4dp, 8dp, 16dp...), padding, corner radii, icon sizes, button heights, and font sizes. Ensures consistent spacing across the entire app.
- **`AppShapesProvider`**: Defines reusable `RoundedCornerShape`s (e.g., `card`, `button`, `bottomSheet`).
---
## 5. Screen Architecture – The MVI Pattern
Every screen follows a consistent **Model-View-Intent (MVI)** pattern, which is a flavour of Unidirectional Data Flow.
### Components of the MVI Pattern
1. **State (`UiState`)** – A **data class** that represents the entire state of the screen (e.g., `HomeUiState`, `SettingsUiState`). It contains all the data needed to render the UI (loading state, lists, booleans, error messages, etc.).
2. **Event (`Event`)** – A **sealed class** that represents user actions or system events (e.g., `SettingsEvent.ToggleHaptic`, `TouchpadEvent.TouchEvent`).
3. **Effect (`Effect`)** – A **sealed class** for one‑time events that should not survive recomposition (e.g., showing a Toast, navigating to another screen, opening a URL).
4. **ViewModel** – The bridge between the UI and the domain/data layers. It holds the `State` and `Effect` flows, handles incoming `Event`s, and executes business logic.
### The Flow (Unidirectional Data Flow)
```mermaid
graph LR
A[User Interaction] --> B[UI sends Event]
B --> C[ViewModel processes Event]
C --> D[ViewModel updates State]
D --> E[UI recomposes with new State]
C --> F[ViewModel emits Effect]
F --> G[UI handles Effect (Toast, Nav)]
```
### Example: `SettingsScreen` in Action
#### 1. State Definition
```kotlin
data class SettingsUiState(
val sensitivity: Float = 0.5f,
val hapticEnabled: Boolean = true,
val theme: String = "system",
// ... 50+ other settings
)
```
#### 2. Event Definition
```kotlin
sealed class SettingsEvent {
data class UpdateSensitivity(val value: Float) : SettingsEvent()
object ToggleHaptic : SettingsEvent()
data class UpdateTheme(val theme: String) : SettingsEvent()
// ...
}
```
#### 3. Effect Definition
```kotlin
sealed class SettingsEffect {
data class ShowToast(val message: String) : SettingsEffect()
data class NavigateTo(val route: String) : SettingsEffect()
}
```
#### 4. ViewModel Logic
```kotlin
@HiltViewModel
class SettingsViewModel @Inject constructor(private val prefs: PreferencesManager) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
private val _effect = MutableSharedFlow<SettingsEffect>()
val effect: SharedFlow<SettingsEffect> = _effect.asSharedFlow()
fun handleEvent(event: SettingsEvent) {
viewModelScope.launch {
when (event) {
is SettingsEvent.UpdateSensitivity -> updateSensitivity(event.value)
SettingsEvent.ToggleHaptic -> toggleHaptic()
// ...
}
}
}
private suspend fun toggleHaptic() {
val current = _uiState.value.hapticEnabled
prefs.putBoolean("haptic_enabled", !current)
_uiState.update { it.copy(hapticEnabled = !current) }
_effect.emit(SettingsEffect.ShowToast("Haptic ${if (!current) "enabled" else "disabled"}"))
}
}
```
#### 5. UI Observation and Event Dispatch
```kotlin
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val effect by viewModel.effect.collectAsStateWithLifecycle(null)
// Handle Effects
LaunchedEffect(effect) {
when (effect) {
is SettingsEffect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
is SettingsEffect.NavigateTo -> navigationActions.navigateTo(effect.route)
null -> { /* ignore */ }
}
}
// Render UI based on State
SettingsSwitch(
title = "Haptic Feedback",
checked = uiState.hapticEnabled,
onCheckedChange = { viewModel.handleEvent(SettingsEvent.ToggleHaptic) }
)
}
```
---
## 6. Deep Dive: Major Screens and Their Unique Mechanisms
### A. `HomeScreen` – The Command Centre
The `HomeScreen` is the most complex UI component. It uses a `LazyColumn` to vertically stack numerous information cards.
- **Real‑time Sensor Data**: It subscribes to `HomeViewModel.sensorState` (which updates at ~30 FPS) to render:
- A 3D `SensorVisualizer` (rotating cube) showing the phone's orientation.
- A 2D `MouseMotionPreviewCard` showing a green square that follows the phone's tilt.
- Live gyroscope charts (`InlineSensorChartsCard`).
- **Connection Management**: The top card (`ConnectionToggleButton`) handles the primary CTA (Connect/Disconnect). The `StateOverviewBanner` shows detailed status (Approval countdown, server IP, calibration status).
- **Collaboration Gate**: The `CollaborationGateCard` displays a donut chart showing calibration progress. It blocks access to certain features until calibration is complete.
- **Quick Actions**: A row of four small cards (`QuickActionsRow`) provides shortcuts to Calibration, Gesture Studio, Voice Commands, and Network Discovery.
### B. `TouchpadScreen` – Full Gesture Surface
This screen turns the phone into a virtual touchpad. It uses the `pointerInput` modifier to capture raw touch events.
- **Event Capture**: `Modifier.pointerInput(Unit) { awaitEachGesture { ... } }` tracks down, move, and up events.
- **Gesture Processing**: The ViewModel analyses the pointer count (1, 2, 3, or 4 fingers) and movement to determine the gesture (Drag, Scroll, Swipe, Pinch, Tap, Long‑Press).
- **Visual Feedback**: If `showTouchPoints` is enabled, the `TouchpadSurface` draws circles at the finger positions with a Canvas.
- **Haptic Feedback**: On tap or gesture completion, the ViewModel triggers `Vibrator` (if enabled).
### C. `SettingsScreen` – The Configuration Hub
This screen uses a `LazyColumn` with a `SettingsCard` for each section. Clicking a card navigates to a `SectionDetailScreen` within the same `Scaffold` (using a `selectedSection` state variable).
- **In‑line Editing**: Cursor and gesture settings use `SettingsSlider` and `SettingsSwitch` composables that directly dispatch events to the ViewModel.
- **Persistent State**: Every slider or switch change immediately writes to `PreferencesManager` via the ViewModel, ensuring settings persist across app restarts.
- **Import/Export**: The Privacy section allows exporting all settings as a `.txt` file and importing them back, using the `ActivityResultContracts.OpenDocument()` launcher.
### D. `CalibrationScreen` – Step‑by‑Step Wizard
This is a state‑driven wizard with three phases: `INTRO`, `COUNTDOWN`, and `SAMPLING`.
- **Phase Management**: The UI changes drastically based on the `CalibrationPhase`. In `SAMPLING`, it shows a pulsing animation and a linear progress bar.
- **Sensor Sampling**: The `CalibrationViewModel` requests sensor data via the `CalibrationHelper` and updates the UI with live gyro/accel values.
- **Quality Assessment**: After completion, it navigates to `CalibrationResultScreen` which displays the `CalibrationQuality` (Excellent, Good, Fair, Poor) with emojis and descriptions.
---
## 7. Reusable UI Component Library
To maintain consistency and speed up development, the app has a rich library of composable components (mostly in `AllUIComponents.kt` and `/components`).
### Atomic Components
- **`GlassCard`**: A card with a blurred, translucent background (glassmorphism).
- **`NeonButton`**: A button with a pulsing glow effect.
- **`AnimatedSwitch` / `AnimatedCheckbox`**: Components with spring animations.
- **`ShimmerEffect`**: A loading placeholder with a moving gradient.
- **`ParticleBackground`**: A background with floating particles.
### Compound Components
- **`ControlDashboard`**: A reusable dashboard card showing connection status, battery, cursor position, and quick actions.
- **`GestureTrainingCenter`**: A card displaying a list of gestures with training progress bars and "Train" buttons.
- **`AnalyticsDashboard`**: Displays a gesture heatmap and activity chart using custom Canvas drawing.
- **`CommunityHub`**: A social feed with posts, likes, and comments.
---
## 8. State Persistence and Lifecycle
- **PreferencesManager**: All settings (sensitivity, theme, haptic, etc.) are stored in `SharedPreferences` (via DataStore or the custom `PreferencesManager` interface). This ensures that when the user re‑opens the app, their preferences are restored.
- **Room Database**: Large datasets (calibration data, gesture templates, user profiles, statistics) are stored in Room. The `LocalDataSourceImpl` handles all CRUD operations.
- **ViewModel Lifecycle**: ViewModels survive configuration changes (screen rotation). They are scoped to the `NavBackStackEntry` of their respective screen, so state is preserved when navigating back.
- **State Restoration**: The `StateFlow` in ViewModels automatically retains the last emitted state. The `collectAsStateWithLifecycle()` API suspends collection when the screen is in the background to save resources.
---
## 9. Interaction Flow: From Touch to Server
To illustrate how the UI connects to the outside world, here is the complete flow of a **touchpad drag**:
1. **User Touch**: User touches the `TouchpadSurface` and drags their finger.
2. **Touch Event**: The `Modifier.pointerInput` block captures the motion event and calls `viewModel.handleEvent(TouchpadEvent.TouchEvent(x, y, pointerCount, pointers, pressure))`.
3. **ViewModel Processing**: `TouchpadViewModel` calculates `dx` and `dy` based on the movement, applies sensitivity, acceleration, and inversion settings.
4. **Command Dispatch**: The ViewModel calls `connectionManager.sendMove(dx, dy)`.
5. **Network Layer**: `ConnectionManager` formats the message as JSON `{"type":"move","dx":12.5,"dy":-3.2}` and sends it via WebSocket/TCP/UDP.
6. **UI Feedback**: Simultaneously, the ViewModel updates the `_uiState` (e.g., updating `currentX`, `currentY`, and `touchPoints`), which triggers a recomposition of the touch points on the Canvas.
---
## 10. Performance Optimisation
- **Throttling**: `HomeViewModel` limits sensor processing to 100Hz (10ms interval) and UI updates to ~30Hz (33ms interval). Move messages are sent at ~60Hz (16ms interval).
- **Lazy Loading**: All scrollable lists use `LazyColumn` or `LazyRow` to only compose visible items.
- **Remember and DeriveState**: Heavy calculations are wrapped in `remember` or `derivedStateOf` to prevent recomputation on every recomposition.
- **Animations**: Uses `AnimatedVisibility` and `animate*AsState` for smooth transitions without blocking the UI thread. Heavy particle systems run on a separate `Canvas` with minimal recomposition.
---
## ✅ Summary
The Air Mouse UI is a **state‑of‑the‑art Jetpack Compose application** that excels in:
- **Reactive UDF**: Unidirectional data flow makes the app predictable and easy to debug.
- **Modularity**: Every screen and component is isolated and reusable.
- **Dynamic Theming**: Full M3 support with custom colours and accent schemes.
- **Real‑time Performance**: Optimised sensor and touch handling for smooth, lag‑free control.
- **Rich Interaction**: Supports complex gestures (click, scroll, pinch, swipe, long‑press) across multiple modes (mouse, touchpad, voice, proximity).
The UI seamlessly ties together the sensor pipeline, network communication, and data persistence, providing a cohesive and polished user experience.

Xet Storage Details

Size:
18.7 kB
·
Xet hash:
8dbf0d65e551a300e4203563f95544e608c1e92e8aa7fd9a1b8232bbfd70aff7

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.