Was macht UX Engineering Guide?
Ein praktisches, produktionserprobtes operatives Framework für das Entwerfen und Entwickeln nativer Android-Anwendungen mit Jetpack Compose, Material 3, Offline-First-Resilienz und menschzentrierter mobiler Ergonomie.
1. Kern-Mobile-Ergonomie & Daumenzone
Mobile Geräte werden zu über 70% der Zeit einhändig gehalten. Gestalten Sie Oberflächen um Daumenreichweite, taktiles Feedback und minimale physische Reibung herum.
1.1 Die ergonomische Erreichbarkeits-Hierarchie
+---------------------------+ <- Oberer Bildschirmrand (Nur Anzeige)
| Status & Header | Am schwierigsten zu erreichen. Reservieren für
| Titel, Quota-Meter | passiven Status, Suchleisten und Live-Messwerte.
+---------------------------+
| | <- Mittlere Reichzone
| Scrollbarer Inhalt | Ideal für Listenelemente, Details und
| Inbox-Karten, Ausschnitte| durchsuchbare Inhaltskarten.
| |
+---------------------------+
| Primäre Aktionsziele | <- Primäre Daumenzone (untere 40%)
| Senden-Buttons, Tabs | Am einfachsten zu tippen. Navigation, CTAs,
| Bottom Sheets, Filter | und interaktive Steuerelemente hier platzieren.
+---------------------------+
1.2 Unverhandelbare Touch-Target-Standards
- Mindestgröße interaktiver Ziele: Jedes klickbare Element muss mindestens $48 \times 48\text{dp}$ (
Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)) groß sein, selbst wenn das visuelle Symbol $24\text{dp}$ beträgt. - Ränder & sichere Bereiche: Wickeln Sie root-Compose-Layouts immer in
Scaffoldein oder wenden SieModifier.windowInsetsPadding(WindowInsets.safeDrawing)an, um Kameraausschnitte, Gesten und Navigationsleisten zu berücksichtigen. - Formularsteuerelemente am unteren Rand: Platzieren Sie Texteingaben, Absende-Buttons und Bestätigungsdialoge in
ModalBottomSheetoder unten fixierten Karten, damit die Softwaretastatur (imePadding()) das aktive Feld nicht verdeckt.
2. Geschwindigkeit & wahrgenommene Leistung
Mobile Nutzer nehmen Verzögerungen sofort wahr. Blockieren Sie nie die Navigation oder zeigen Sie leere Ladezustände an, wenn zuvor abgerufene Daten verfügbar sind.
2.1 Die Null-Skeleton-Regel (Stale-While-Revalidate)
- Das Problem: Jedes Mal, wenn der Nutzer auf den Inbox-Tab tippt, ein ganzseitiges Skeleton-Ladegerüst oder einen kreisförmigen Spinner anzuzeigen, fühlt sich langsam, ruckelig und unausgereift an.
- Die Lösung: Implementieren Sie ein Stale-While-Revalidate (SWR) Cache-Muster:
- Speichern Sie abgerufene Elemente im lokalen verschlüsselten Speicher (
EncryptedSharedPreferencesoder SQLite/Room). - Beim Betreten des Bildschirms cachierte Elemente sofort rendern (0ms Verzögerung).
- Revalidieren Sie im Hintergrund, wenn der Cache älter als 5–10 Minuten ist.
- Wenn aktualisierte Daten eintreffen, Elemente fließend anhängen/aktualisieren, ohne die Liste zu leeren oder leere Platzhalter anzuzeigen.
- Zeigen Sie nur subtile Indikatoren (z. B. einen Pull-to-Refresh-Spinner oder eine kleine Fortschrittsleiste oben) während Hintergrundaktualisierungen an.
- Speichern Sie abgerufene Elemente im lokalen verschlüsselten Speicher (
2.2 Offline-First-Outbox-Muster
Lassen Sie eine Benutzeraktion niemals fehlschlagen, nur weil das Mobilgerät eine unzuverlässige Mobilfunkverbindung hat oder vorübergehend im Flugmodus ist.
flowchart TD
UserAction["Benutzer teilt Datei / schreibt Notiz"] --> LocalDraft["1. In lokale Outbox-Entwürfe speichern"]
LocalDraft --> RenderOutbox["2. Sofort in Outbox-Warteschlangenkarte rendern"]
RenderOutbox --> CheckNet{"Mit Netzwerk verbunden?"}
CheckNet -- Ja --> DirectUpload["3. Direkt hochladen"]
CheckNet -- Nein --> EnqueueWork["3. WorkManager-Job einreihen (NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. Auf WLAN / Mobilfunk warten"]
WaitConn --> AutoDispatch["5. Hintergrundupload mit exponentiellem Backoff"]
DirectUpload --> Success["6. UI-Status aktualisieren & Toast benachrichtigen"]
AutoDispatch --> Success
Outbox-UI-Zustände:
- Ausstehend: "Warten auf Netzwerk…" mit einem pausierten Cloud-Symbol.
- Wird hochgeladen: "Sende 42%…" mit einem linearen Fortschrittsindikator.
- Fehlgeschlagen / Wiederholung: Klarer Statusindikator mit expliziten Wiederholen- und Verwerfen-Buttons.
3. Bewegung, Übergänge & taktile Haptik
Modernes Android-Design (Android 14/15+) erfordert dynamische, physikbasierte Bewegungen und taktiles sensorisches Feedback, um sich nativ und reaktionsschnell anzufühlen.
3.1 Edge-to-Edge & Systemleisten-Handhabung
Zeichnen Sie immer global hinter den Systemleisten in MainActivity.kt:
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHost(Modifier.padding(innerPadding))
}
}
}
3.2 Vorausschauende Zurück-Gesten
Aktivieren Sie "Predictive Back" in AndroidManifest.xml (android:enableOnBackInvokedCallback="true") und verwenden Sie PredictiveBackHandler für Bottom Sheets und Detail-Drawer:
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// Detailblatt proportional zu backEvent.progress (0f bis 1f) skalieren oder verschieben
detailScale = 1f - (backEvent.progress * 0.1f)
}
// Benutzer hat Zurück-Geste ausgeführt
closeDetail()
} catch (e: CancellationException) {
// Benutzer hat Zurück-Geste abgebrochen, zurückspringen auf 1f
detailScale = 1f
}
}
3.3 Shared-Element-Übergänge
Beim Übergang zwischen einer Inbox-Listenzeile und einer Artikel-Detailansicht umschließen Sie die Bildschirme mit SharedTransitionLayout:
SharedTransitionLayout {
NavHost(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
Verwenden Sie .sharedElement() oder .sharedBounds() auf Miniaturbildkarten, Avataren und Titeln, um nahtlose Übergänge zu erzeugen.
3.4 Taktile Haptik
Lassen Sie Benutzeraktionen niemals still, wenn ein physischer Meilenstein eintritt:
- Schwellenwert-Wischbewegungen (Swipe-to-Dismiss): Lösen Sie
HapticFeedbackConstantsCompat.GESTURE_STARTaus, wenn die Wischbewegung die Aktionsschwelle überschreitet. - In Zwischenablage kopieren / Senden abgeschlossen: Lösen Sie
HapticFeedbackConstantsCompat.CONFIRMaus. - Validierungsfehler / Netzwerkfehler: Lösen Sie
HapticFeedbackConstantsCompat.REJECTaus.
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 Federphysik statt linearer Easing
Vermeiden Sie starre lineare Animationen. Verwenden Sie immer Federphysik für Gesten, Sheets und Entlassungen:
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. Intelligente One-Tap-Aktionen & Systemintegration
Mobile Oberflächen sollten die kognitive Belastung minimieren, indem sie den Payload-Typ erkennen und die relevanteste native Systemaktion anbieten.
4.1 Kontextbewusste Aktionsmatrix
| Payload-Typ | Primäre Aktion | Sekundäre Aktion | Systemintegration |
|---|---|---|---|
| URL / Weblink | Im Browser öffnen (Intent.ACTION_VIEW) | Link kopieren | Chrome Custom Tabs oder Standardbrowser des Systems |
| Text / Ausschnitt | Text kopieren | Teilen (Intent.ACTION_SEND) | Android ClipboardManager mit sofortigem Toast / Snackbar |
| Datei / Medium | In Ordner herunterladen | Datei öffnen | Storage Access Framework (SAF) + FileProvider |
4.2 Direkte Dateidownloads mit benutzerdefiniertem Standardverzeichnis
Den Benutzer jedes Mal beim Herunterladen einer Datei mit der Systemdateiauswahl aufzufordern, erzeugt unnötige Reibung.
- Erlauben Sie dem Benutzer, einmalig in den Einstellungen/Hub einen Standard-Download-Ordner mit
ActivityResultContracts.OpenDocumentTree()auszuwählen. - Persistieren Sie die Berechtigung dauerhaft:
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - Beim Tippen auf Download:
- Wenn ein Standardverzeichnis festgelegt ist: Streamen Sie Bytes direkt mit
DocumentFile.createFile(), zeigen Sie live Byte-Fortschritt an und feuern Sie einen nativen Abschluss-Toast ("document.pdf wurde in Vault heruntergeladen"). - Wenn kein Standard festgelegt ist: Fallback auf
ActivityResultContracts.CreateDocument().
- Wenn ein Standardverzeichnis festgelegt ist: Streamen Sie Bytes direkt mit
4.3 Sicheres Öffnen von Dateien mit FileProvider
Verwenden Sie niemals unsichere rohe file://-URIs. Delegieren Sie die Dateiansicht immer an native Android-Anwendungen mit FileProvider:
<!-- res/xml/file_paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cached_files" path="." />
</paths>
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "Öffnen mit…"))
5. Reibungslose mobile Authentifizierung
Das Eintippen von Server-URLs und langen Passwörtern auf Mobilbildschirmen ist anfällig für Tippfehler.
5.1 Login-Prioritätshierarchie
- Priorität 1: QR-Code-Kamera-Scan (1-Sekunden-Pairing)
- Zeigen Sie einen temporären, signierten Pairing-QR-Code in der Desktop-Web-App an.
- Der mobile Benutzer öffnet die App, tippt auf QR-Code scannen, richtet die Kamera auf den Bildschirm.
- Die App parst Herkunft, Token und Gerätenamen in einem einzigen Durchgang – sofort authentifiziert.
- Priorität 2: Benutzername & Passwort mit einklappbarer Server-URL
- 99% der Logins verwenden den standardmäßig konfigurierten Host.
- Standardmäßig nur Benutzername und Passwort-Felder anzeigen.
- Die Server-URL hinter einem dezenten einklappbaren Umschalter verstecken:
"Serverkonfiguration (fortgeschritten)".
5.2 14-tägige gleitende Sitzungspersistenz
- Speichern Sie Access- (
va1_) und Refresh- (vr1_) Token in AndroidEncryptedSharedPreferences. - Rotieren Sie das Refresh-Token bei aktiver Nutzung automatisch gleitend.
- Solange der Benutzer die App innerhalb von 14 Tagen öffnet, bleibt er ohne erneute Authentifizierung eingeloggt.
6. Adaptive Layouts (Foldables & Tablets)
Erstellen Sie Bildschirme, die sich mit WindowSizeClass nahtlos an verschiedene Fenstergrößenklassen anpassen:
- Kompakt (
<600dp): Einspaltig, untere Navigationsleiste, modale Bottom Sheets. - Mittel (
600dp - 840dp): Navigationsschiene, zweispaltiges Raster. - Erweitert (
>840dp): Festes Master-Detail-Zweifachlayout nebeneinander (Liste links, Inspektor rechts).
7. Material 3 Farbkontrast & Zustandsbarrierefreiheit (WCAG 2.2 AA / AAA)
Mobile Geräte sind aufgrund wechselnder Umgebungsbeleuchtung (direktes Sonnenlicht, Außenblendung, OLED-Batteriespar-Dimmung) besonders anfällig für Kontrastfehler. Streben Sie WCAG 2.2 AAA-Textkontrast ($\ge 7:1$) und strikte Einhaltung von WCAG 1.4.11 (Nicht-Text-Grenzkontrast $\ge 3:1$) und WCAG 1.4.1 (Verwendung von Farbe) an.
7.1 Die Material 3 Dark-Mode-FilterChip-Falle
- Der Fehler: Standardmäßig verwendet Material 3
FilterChipim DunkelmodussecondaryContainer(#37474F) für seinen ausgewählten Hintergrund gegen die dunklesurface(#14181E). Dies ergibt ein inakzeptables Kontrastverhältnis von 1,83:1. Bei niedriger Helligkeit oder Blendung können Benutzer nicht erkennen, welcher Chip aktiv ist. - Die Behebung:
- Erzwingen Sie eine kontrastreiche Grenzdefinition mit
selectedBorderColor = MaterialTheme.colorScheme.primary(was ein 9,88:1 Kontrastverhältnis im Dunkelmodus ergibt). - Setzen Sie
selectedBorderWidth = 1.5.dp. - Erfüllen Sie WCAG 1.4.1 (Zustände, die nicht nur von Farbe abhängen) durch ein führendes Häkchen-Symbol:
FilterChip( selected = isSelected, onClick = { onSelect() }, label = { Text(label) }, leadingIcon = if (isSelected) { { Icon(Icons.Filled.Done, contentDescription = null, Modifier.size(16.dp)) } } else null, border = FilterChipDefaults.filterChipBorder( enabled = true, selected = isSelected, selectedBorderColor = MaterialTheme.colorScheme.primary, selectedBorderWidth = 1.5.dp ) )
- Erzwingen Sie eine kontrastreiche Grenzdefinition mit
7.2 Sonnenlicht- & Blendungslesbarkeitsstandards
- Primärtext vs. Oberfläche: Standardisieren Sie den Dunkelmodus auf kontrastreichen Text (
onSurface#E1E2E8oder#F8FAFCauf#14181E), um $\ge 13:1$ Kontrast zu erreichen (übertrifft WCAG AAA). - Formularsteuerelement-Umrisse:
OutlinedTextFieldsowie Radio-/Checkbox-Rahmen müssenoutline-Tokens verwenden, die mindestens $3:1$ Kontrast gegen Containerkarten (Color(0xFF8D9199)oder explizitrgba(255, 255, 255, 0.20)) aufweisen. - Kompakte Voreinstellungszeilen: Beim Layouten kompakter Chip-Zeilen (z. B. Aufbewahrungsvoreinstellungen
1h,4h,12h,1d), bewahren Sie Zielgrößen und wenden SiemaxLines = 1mitselectedBorderColoran, damit Chips nie umbrechen oder mehrdeutig aussehen.
8. Referenz-Open-Source-Repositories
Untersuchen Sie diese branchenführenden Open-Source-Android-Projekte zur Inspiration für Design und Implementierung:
android/nowinandroid: Googles Flaggschiff-Material-3-Compose-Beispiel mit dynamischer Designanpassung, adaptiven Layouts und sauberer Architektur.skydoves/Pokedex-Compose: Produktionsreife Shared-Element-Übergänge (.sharedBounds/.sharedElement) und flüssige Detail-Sheet-Gesten.plainhub/plain-app: Herausragende Implementierung lokaler Telefonverwaltung, Material You-Design und responsiver Client-Server-Synchronisation.Gurupreet/ComposeCookBook: Erschöpfender Katalog produktionsreifer Compose-Animationen, unterer Leisten und benutzerdefinierter Canvas-Mikrointeraktionen.signalapp/Signal-Android: Goldstandard für lokale verschlüsselte Speicherung, Hintergrund-Outbox-Worker und Netzwerk-Resilienz.