Initial project import and stabilization baseline
This commit is contained in:
883
admin-ui-brief.md
Normal file
883
admin-ui-brief.md
Normal file
@@ -0,0 +1,883 @@
|
||||
# Flutter TV Admin UI: Best Practices & Implementation Guide
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Objective**: Build a world-class admin dashboard experience for Android TV using Flutter, maintaining the single-APK architecture while delivering excellent UX despite TV input constraints.
|
||||
|
||||
**Key Insight**: Flutter is fully capable of building excellent TV dashboards. The cross-platform nature doesn't limit quality - it means we can build platform-optimized experiences with proper focus management and TV-specific widgets.
|
||||
|
||||
**Target Platform**: Android TV Box (1920x1080 landscape, D-pad navigation, no touch input)
|
||||
|
||||
---
|
||||
|
||||
## 1. Understanding Android TV UX Constraints
|
||||
|
||||
### Critical Challenges
|
||||
|
||||
```
|
||||
❌ No touch input (D-pad navigation only)
|
||||
❌ Awkward text input (on-screen keyboard via remote)
|
||||
❌ Screen distance (2-3 meters from viewer)
|
||||
❌ Limited input methods (remote control only)
|
||||
❌ No mouse hover states
|
||||
❌ Small text is unreadable
|
||||
```
|
||||
|
||||
### Our Advantages
|
||||
|
||||
```
|
||||
✅ Flutter has excellent focus management built-in
|
||||
✅ Rich animation and transition support
|
||||
✅ Custom widgets are straightforward to build
|
||||
✅ TV-specific packages available
|
||||
✅ We control the entire experience
|
||||
✅ Hardware acceleration for smooth animations
|
||||
✅ Scalable UI via MediaQuery
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Focus Management (The Foundation)
|
||||
|
||||
Focus management is **the most critical** aspect of TV UI. All interactions flow through proper focus handling.
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Always show focus**: Users must see what's currently selected
|
||||
2. **Predictable navigation**: D-pad moves in expected directions
|
||||
3. **Large focus targets**: Everything must be easily selectable
|
||||
4. **Clear visual feedback**: Animated focus indicators
|
||||
5. **Sound feedback**: Audible confirmation of focus changes
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```dart
|
||||
// All TV widgets should follow this pattern:
|
||||
class TVWidget extends StatefulWidget {
|
||||
@override
|
||||
State<TVWidget> createState() => _TVWidgetState();
|
||||
}
|
||||
|
||||
class _TVWidgetState extends State<TVWidget> {
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
|
||||
// Listen to focus changes for visual feedback
|
||||
_focusNode.addListener(() {
|
||||
setState(() {
|
||||
// Update UI when focus changes
|
||||
});
|
||||
|
||||
// Play sound feedback
|
||||
if (_focusNode.hasFocus) {
|
||||
TVSoundService().playFocusSound();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
focusNode: _focusNode,
|
||||
onKeyEvent: (node, event) {
|
||||
// Handle D-pad navigation
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final isFocused = _focusNode.hasFocus;
|
||||
// Build focused/unfocused states
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. TV Scaling System
|
||||
|
||||
All dimensions must scale relative to screen width to ensure consistency across different TV sizes.
|
||||
|
||||
```dart
|
||||
class TVScaling {
|
||||
static double scale(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width / 1920;
|
||||
}
|
||||
|
||||
// Helper methods for common scaled values
|
||||
static double padding(BuildContext context) => 32 * scale(context);
|
||||
static double gap(BuildContext context) => 24 * scale(context);
|
||||
static double borderRadius(BuildContext context) => 12 * scale(context);
|
||||
|
||||
// Typography scale
|
||||
static double fontSizeHeading(BuildContext context) => 48 * scale(context);
|
||||
static double fontSizeTitle(BuildContext context) => 36 * scale(context);
|
||||
static double fontSizeBody(BuildContext context) => 28 * scale(context);
|
||||
static double fontSizeCaption(BuildContext context) => 22 * scale(context);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. TV-Optimized Form Widgets
|
||||
|
||||
### A. TV TextField
|
||||
|
||||
**Key Features:**
|
||||
- Extra large hit targets (minimum 48x48dp)
|
||||
- Clear focus indication (border + background + shadow)
|
||||
- Keyboard hint for users
|
||||
- Auto-scroll into view when focused
|
||||
- Sound feedback on focus
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
TVTextField(
|
||||
label: 'Nama Masjid',
|
||||
value: settings.masjidName,
|
||||
onChanged: (value) => updateSetting('masjidName', value),
|
||||
hint: 'Masjid Ar-Rahman',
|
||||
autofocus: false,
|
||||
)
|
||||
```
|
||||
|
||||
### B. TV Selection Field
|
||||
|
||||
**Key Features:**
|
||||
- Dropdown/selector pattern (avoids typing)
|
||||
- Search functionality for long lists
|
||||
- Clear visual hierarchy
|
||||
- Keyboard navigation (arrow keys)
|
||||
- Selected state indication
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
TVSelectionField<String>(
|
||||
label: 'Pilih Lokasi',
|
||||
value: settings.cityIdApi,
|
||||
options: [
|
||||
TVSelectionOption(
|
||||
label: 'Kota Yogyakarta',
|
||||
value: '577ef1154f3240ad5b9b413aa7346a1e',
|
||||
subtitle: 'Daerah Istimewa Yogyakarta',
|
||||
icon: Icons.location_city,
|
||||
),
|
||||
TVSelectionOption(
|
||||
label: 'Jakarta Pusat',
|
||||
value: 'another_id_here',
|
||||
subtitle: 'DKI Jakarta',
|
||||
icon: Icons.location_city,
|
||||
),
|
||||
],
|
||||
onChanged: (value) => saveLocation(value),
|
||||
)
|
||||
```
|
||||
|
||||
### C. TV Button
|
||||
|
||||
**Key Features:**
|
||||
- Large touch targets (minimum 48x48dp)
|
||||
- Multiple style variants (primary, secondary, danger)
|
||||
- Icon + label support
|
||||
- Animated focus feedback
|
||||
- Sound on press
|
||||
- Keyboard activation (Enter/Select)
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
TVButton(
|
||||
label: 'Simpan Pengaturan',
|
||||
icon: Icons.save,
|
||||
style: TVButtonStyle.primary,
|
||||
onPressed: () => saveSettings(),
|
||||
)
|
||||
|
||||
// Icon-only variant
|
||||
TVButton.icon(
|
||||
icon: Icons.arrow_back,
|
||||
label: 'Kembali',
|
||||
onPressed: () => Navigator.pop(context),
|
||||
)
|
||||
```
|
||||
|
||||
### D. TV Toggle Switch
|
||||
|
||||
**Key Features:**
|
||||
- Large clickable area (entire row)
|
||||
- Clear on/off state
|
||||
- Animated transition
|
||||
- Focus indication
|
||||
- Label with description
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
TVSwitch(
|
||||
label: 'Auto Kalibrasi Online',
|
||||
subtitle: 'Sinkronisasi waktu otomatis saat online',
|
||||
value: settings.autoCalibrationEnabled,
|
||||
onChanged: (value) => updateSetting('autoCalibration', value),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Layout Patterns
|
||||
|
||||
### A. Grid-Based Dashboard
|
||||
|
||||
**Purpose**: Main admin navigation screen
|
||||
|
||||
**Characteristics:**
|
||||
- 3-column grid layout
|
||||
- Large cards with icons
|
||||
- Clear visual hierarchy
|
||||
- Easy D-pad navigation
|
||||
- Focus flows naturally left-to-right, top-to-bottom
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────┐
|
||||
│ Location │ Appearance │ Settings │
|
||||
│ & Schedule│ │ │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ Rotation │ About │ │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
### B. Wizard-Style Multi-Step Forms
|
||||
|
||||
**Purpose**: Break complex forms into manageable steps
|
||||
|
||||
**Benefits:**
|
||||
- Reduces cognitive load
|
||||
- Easier navigation
|
||||
- Progress indication
|
||||
- Can save state between steps
|
||||
- Less overwhelming than long forms
|
||||
|
||||
**When to Use:**
|
||||
- Initial setup wizard
|
||||
- Complex configuration with multiple sections
|
||||
- Settings that require multiple steps
|
||||
- Data entry with validation
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
Step 1: Identity → Step 2: Location → Step 3: Appearance → Step 4: Confirm
|
||||
↓ ↓ ↓ ↓
|
||||
[Masjid Name] [Select City] [Theme Color] [Review All]
|
||||
[Address] [Sync Data] [Background] [Save & Exit]
|
||||
```
|
||||
|
||||
### C. Tabbed Interface
|
||||
|
||||
**Purpose**: Organize related settings
|
||||
|
||||
**Benefits:**
|
||||
- Clear categorization
|
||||
- Easy to add new sections
|
||||
- Familiar pattern from web
|
||||
- Works well with D-pad
|
||||
|
||||
**Tab Structure:**
|
||||
1. **Identitas** - Mosque name, address
|
||||
2. **Jadwal & Sync** - Location, synchronization
|
||||
3. **Tampilan** - Theme, background, slideshow
|
||||
4. **Rotasi** - Screen timing
|
||||
5. **Pembarisan** - Updates, version
|
||||
6. **Tentang** - App information
|
||||
|
||||
---
|
||||
|
||||
## 6. Reducing Text Input (The Secret Sauce)
|
||||
|
||||
The best TV admin interfaces **minimize typing**. This is crucial because:
|
||||
|
||||
1. TV remotes are terrible for text input
|
||||
2. On-screen keyboards are slow and error-prone
|
||||
3. Screen distance makes typing difficult
|
||||
4. User frustration increases with typing
|
||||
|
||||
### Strategies to Minimize Typing
|
||||
|
||||
#### A. Use Selection Instead of Text Input
|
||||
|
||||
**Bad (Text Input):**
|
||||
```dart
|
||||
TextField(
|
||||
decoration: InputDecoration(labelText: 'City ID API'),
|
||||
)
|
||||
```
|
||||
|
||||
**Good (Selection):**
|
||||
```dart
|
||||
TVSelectionField<String>(
|
||||
label: 'Pilih Kota',
|
||||
value: settings.cityIdApi,
|
||||
options: cityOptions,
|
||||
onChanged: (value) => saveCity(value),
|
||||
)
|
||||
```
|
||||
|
||||
#### B. Search + Select Pattern
|
||||
|
||||
For long lists (like 500+ Indonesian cities):
|
||||
|
||||
```dart
|
||||
TVSearchSelectField<String>(
|
||||
label: 'Cari Lokasi',
|
||||
hint: 'Ketik nama kota...',
|
||||
searchDelegate: MyQuranCitySearchDelegate(),
|
||||
onSelect: (city) => saveCity(city),
|
||||
)
|
||||
```
|
||||
|
||||
#### C. Preset Options with Custom Value
|
||||
|
||||
```dart
|
||||
TVSelectionField<String>(
|
||||
label: 'Warna Tema',
|
||||
value: settings.themeColor,
|
||||
options: [
|
||||
TVSelectionOption(label: 'Hijau', value: '0xFF006400'),
|
||||
TVSelectionOption(label: 'Biru', value: '0xFF0000FF'),
|
||||
TVSelectionOption(label: 'Merah', value: '0xFFFF0000'),
|
||||
TVSelectionOption(label: 'Emas', value: '0xFFFFD700'),
|
||||
TVSelectionOption(label: 'Kustom...', value: 'custom'),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == 'custom') {
|
||||
// Show color picker dialog
|
||||
} else {
|
||||
saveThemeColor(value);
|
||||
}
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### D. Slider for Numeric Values
|
||||
|
||||
**Bad (Number Input):**
|
||||
```dart
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(labelText: 'Durasi Iqomah (menit)'),
|
||||
)
|
||||
```
|
||||
|
||||
**Good (Slider):**
|
||||
```dart
|
||||
TVSlider(
|
||||
label: 'Durasi Iqomah',
|
||||
value: settings.iqomahDuration.inMinutes,
|
||||
min: 5,
|
||||
max: 20,
|
||||
divisions: 15,
|
||||
unit: 'menit',
|
||||
onChanged: (value) => saveIqomahDuration(Duration(minutes: value)),
|
||||
)
|
||||
```
|
||||
|
||||
#### E. Time Picker
|
||||
|
||||
```dart
|
||||
TVTimePicker(
|
||||
label: 'Waktu Mulai Subuh',
|
||||
value: settings.subuhStartTime,
|
||||
onChanged: (value) => saveSubuhStartTime(value),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Visual Design Guidelines
|
||||
|
||||
### Focus Indicators
|
||||
|
||||
Focus indicators must be:
|
||||
- **High contrast** (visible from 2-3 meters)
|
||||
- **Animated** (smooth transitions, ~200ms)
|
||||
- **Multi-layered** (border + background + shadow)
|
||||
- **Consistent** (same pattern throughout app)
|
||||
|
||||
**Recommended Focus States:**
|
||||
|
||||
```dart
|
||||
// Unfocused
|
||||
border: 1px solid grey
|
||||
background: white
|
||||
shadow: none
|
||||
|
||||
// Focused
|
||||
border: 3px solid primary_color
|
||||
background: primary_color_10_percent
|
||||
shadow: primary_color_30_percent_blur_20px
|
||||
```
|
||||
|
||||
### Typography Scale
|
||||
|
||||
```dart
|
||||
// Based on 1920px width baseline
|
||||
Display (Clock): 120px // For large time displays
|
||||
Heading: 48px // Screen titles
|
||||
Title: 36px // Section headers, button labels
|
||||
Body: 28px // Form labels, content
|
||||
Caption: 22px // Metadata, hints
|
||||
```
|
||||
|
||||
### Color Contrast
|
||||
|
||||
- **Minimum contrast ratio**: 4.5:1 (WCAG AA)
|
||||
- **Recommended contrast ratio**: 7:1 (WCAG AAA)
|
||||
- **Focus indication**: Always use high contrast
|
||||
- **Error states**: Red with white text
|
||||
- **Success states**: Green with white text
|
||||
|
||||
### Spacing System
|
||||
|
||||
```dart
|
||||
// Based on 8px grid system
|
||||
padding_xs: 8px // Tight spacing
|
||||
padding_sm: 16px // Compact
|
||||
padding_md: 24px // Default
|
||||
padding_lg: 32px // Comfortable
|
||||
padding_xl: 48px // Generous
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Navigation Patterns
|
||||
|
||||
### D-Pad Navigation Flow
|
||||
|
||||
```
|
||||
↑
|
||||
↑
|
||||
← [Focus] →
|
||||
↓
|
||||
↓
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
1. **Natural flow**: Left-to-right, top-to-bottom
|
||||
2. **Wrap around**: Last item → first item in same row
|
||||
3. **Section boundaries**: Clear visual separators
|
||||
4. **Skip separators**: Focus should jump over dividers
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
```dart
|
||||
// Standard Android TV shortcuts
|
||||
KEY_ENTER / KEY_SELECT: Activate focused item
|
||||
KEY_BACK: Go back / cancel
|
||||
KEY_MENU: Show context menu
|
||||
KEY_MEDIA_PLAY_PAUSE: Play/pause media
|
||||
KEY_MEDIA_FAST_FORWARD: Skip forward
|
||||
KEY_MEDIA_REWIND: Skip back
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Feedback Systems
|
||||
|
||||
### Visual Feedback
|
||||
|
||||
1. **Focus indicator**: Border + background + shadow
|
||||
2. **Pressed state**: Scale down slightly (0.95x)
|
||||
3. **Loading state**: Progress indicator + "Loading..." text
|
||||
4. **Success state**: Green checkmark + success message
|
||||
5. **Error state**: Red X + error message
|
||||
|
||||
### Sound Feedback
|
||||
|
||||
```dart
|
||||
// Sound effects for different interactions
|
||||
focus.mp3: Subtle "click" when focus moves
|
||||
select.mp3: Confirming "ding" when item activated
|
||||
error.mp3: Low "buzz" for errors
|
||||
success.mp3: Pleasant "chime" for success
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```dart
|
||||
class TVSoundService {
|
||||
static final TVSoundService _instance = TVSoundService._internal();
|
||||
factory TVSoundService() => _instance;
|
||||
TVSoundService._internal();
|
||||
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
Future<void> playFocus() => _audioPlayer.play(AssetSource('sounds/focus.mp3'));
|
||||
Future<void> playSelect() => _audioPlayer.play(AssetSource('sounds/select.mp3'));
|
||||
Future<void> playError() => _audioPlayer.play(AssetSource('sounds/error.mp3'));
|
||||
Future<void> playSuccess() => _audioPlayer.play(AssetSource('sounds/success.mp3'));
|
||||
}
|
||||
```
|
||||
|
||||
### Haptic Feedback
|
||||
|
||||
```dart
|
||||
// For game controllers with vibration
|
||||
if (_focusNode.hasFocus) {
|
||||
HapticFeedback.lightImpact(); // Subtle vibration on focus
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Performance Considerations
|
||||
|
||||
### Animation Performance
|
||||
|
||||
```dart
|
||||
// Use AnimatedBuilder for efficient rebuilds
|
||||
AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: ExpensiveWidget(), // Built only once
|
||||
)
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```dart
|
||||
// Use ListView.builder for long lists
|
||||
ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return TVListItem(item: items[index]);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Image Optimization
|
||||
|
||||
```dart
|
||||
// Cache images, use placeholders
|
||||
CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
placeholder: (context, url) => TVLoadingIndicator(),
|
||||
errorWidget: (context, url, error) => TVErrorIcon(),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Accessibility
|
||||
|
||||
### Screen Reader Support
|
||||
|
||||
```dart
|
||||
// Add semantic labels
|
||||
Semantics(
|
||||
label: 'Nama masjid field',
|
||||
value: settings.masjidName,
|
||||
hint: 'Masukkan nama masjid',
|
||||
child: TVTextField(...),
|
||||
)
|
||||
```
|
||||
|
||||
### High Contrast Mode
|
||||
|
||||
```dart
|
||||
// Respect system high contrast setting
|
||||
final isHighContrast = MediaQuery.of(context).highContrast;
|
||||
|
||||
if (isHighContrast) {
|
||||
// Use black and white only
|
||||
// Remove subtle gradients
|
||||
// Increase border widths
|
||||
}
|
||||
```
|
||||
|
||||
### Font Scaling
|
||||
|
||||
```dart
|
||||
// Respect user's font size preferences
|
||||
final scaleFactor = MediaQuery.of(context).textScaleFactor;
|
||||
|
||||
Text(
|
||||
'Hello',
|
||||
style: TextStyle(fontSize: 28 * scaleFactor),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Implementation Roadmap
|
||||
|
||||
### Phase 1: Foundation (Week 1)
|
||||
|
||||
**Focus Management System**
|
||||
- [ ] Create `TVFocusManager` widget
|
||||
- [ ] Implement focus scope nodes
|
||||
- [ ] Add focus listener utilities
|
||||
- [ ] Create focus ring animation
|
||||
- [ ] Test D-pad navigation
|
||||
|
||||
**Scaling System**
|
||||
- [ ] Create `TVScaling` utility
|
||||
- [ ] Implement responsive layout helpers
|
||||
- [ ] Define typography scale
|
||||
- [ ] Test on different screen sizes
|
||||
|
||||
### Phase 2: Core Widgets (Week 2)
|
||||
|
||||
**Form Widgets**
|
||||
- [ ] `TVTextField` with focus management
|
||||
- [ ] `TVSelectionField` with dropdown
|
||||
- [ ] `TVButton` with multiple styles
|
||||
- [ ] `TVSwitch` for toggles
|
||||
- [ ] `TVSlider` for numeric values
|
||||
|
||||
**Layout Widgets**
|
||||
- [ ] `TVDashboardTile` for grid items
|
||||
- [ ] `TVListItem` for list items
|
||||
- [ ] `TVCard` for containers
|
||||
- [ ] `TVSection` for grouping
|
||||
|
||||
### Phase 3: Advanced Features (Week 3)
|
||||
|
||||
**Navigation & Layouts**
|
||||
- [ ] Grid-based dashboard screen
|
||||
- [ ] Wizard-style multi-step form
|
||||
- [ ] Tabbed interface
|
||||
- [ ] Breadcrumb navigation
|
||||
|
||||
**Input Optimization**
|
||||
- [ ] `TVSearchSelectField` with search
|
||||
- [ ] `TVTimePicker` for time selection
|
||||
- [ ] `TVDatePicker` for date selection
|
||||
- [ ] `TVColorPicker` for color selection
|
||||
|
||||
### Phase 4: Polish (Week 4)
|
||||
|
||||
**Feedback Systems**
|
||||
- [ ] Sound effects (focus, select, error, success)
|
||||
- [ ] Visual feedback animations
|
||||
- [ ] Loading indicators
|
||||
- [ ] Error state UIs
|
||||
- [ ] Success state UIs
|
||||
|
||||
**Performance**
|
||||
- [ ] Animation optimization
|
||||
- [ ] Image caching
|
||||
- [ ] Lazy loading
|
||||
- [ ] Memory leak testing
|
||||
|
||||
**Accessibility**
|
||||
- [ ] Screen reader support
|
||||
- [ ] High contrast mode
|
||||
- [ ] Font scaling
|
||||
- [ ] Focus indicators for color blind users
|
||||
|
||||
---
|
||||
|
||||
## 13. Testing Guidelines
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```dart
|
||||
testWidgets('TVTextField should show focus indicator', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: TVTextField(label: 'Test')),
|
||||
);
|
||||
|
||||
// Simulate focus
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pump();
|
||||
|
||||
// Verify focus indicator is visible
|
||||
expect(find.byType(Border), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```dart
|
||||
testWidgets('Complete admin flow should work', (tester) async {
|
||||
// Build admin screen
|
||||
await tester.pumpWidget(MyApp());
|
||||
|
||||
// Navigate to location settings
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select city
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify selection was saved
|
||||
expect(find.text('Kota Yogyakarta'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [ ] All widgets are focusable with D-pad
|
||||
- [ ] Focus indicators are clearly visible
|
||||
- [ ] Navigation flow is predictable
|
||||
- [ ] Text input works (though awkward)
|
||||
- [ ] Sound feedback plays correctly
|
||||
- [ ] Animations are smooth (60fps)
|
||||
- [ ] Forms can be completed without mouse/touch
|
||||
- [ ] Error states are clear
|
||||
- [ ] Success states provide feedback
|
||||
- [ ] Back button exits correctly
|
||||
|
||||
---
|
||||
|
||||
## 14. Common Pitfalls to Avoid
|
||||
|
||||
### ❌ Don't
|
||||
|
||||
1. **Don't use small hit targets**: Everything must be at least 48x48dp
|
||||
2. **Don't rely on hover states**: TV remotes don't have hover
|
||||
3. **Don't use subtle gradients**: Hard to see from distance
|
||||
4. **Don't require extensive typing**: It's frustrating on TV
|
||||
5. **Don't use small fonts**: Under 24px is unreadable from 2m
|
||||
6. **Don't hide navigation**: Always show clear focus indicators
|
||||
7. **Don't use web-only patterns**: TV ≠ web browser
|
||||
8. **Don't forget sound feedback**: Crucial for accessibility
|
||||
9. **Don't ignore keyboard shortcuts**: Power users expect them
|
||||
10. **Don't make users backtrack**: Forward-flow navigation only
|
||||
|
||||
### ✅ Do
|
||||
|
||||
1. **Do make focus obvious**: Large borders, backgrounds, shadows
|
||||
2. **Do provide clear feedback**: Visual + audio + haptic
|
||||
3. **Do minimize text input**: Use selectors, wizards, presets
|
||||
4. **Do use large fonts**: Minimum 24px, recommended 28px+
|
||||
5. **Do test on real TV**: Emulator ≠ real experience
|
||||
6. **Do optimize for D-pad**: Natural navigation flow
|
||||
7. **Do add sound effects: Helpful feedback
|
||||
8. **Do use animations: Smooth transitions (200ms)
|
||||
9. **Do provide shortcuts: Keyboard navigation
|
||||
10. **Do think in steps: Break complex forms into wizards
|
||||
|
||||
---
|
||||
|
||||
## 15. Recommended Packages
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
# TV-specific components
|
||||
flutter_tencent_eui: ^latest # TV-optimized UI components
|
||||
|
||||
# Enhanced focus management
|
||||
flutter_focus: ^latest # Better focus control
|
||||
|
||||
# Sound feedback
|
||||
audioplayers: ^latest # Sound effects
|
||||
|
||||
# Animations
|
||||
animations: ^latest # Smooth transition helpers
|
||||
|
||||
# Form validation
|
||||
form_field_validator: ^latest # Reduce user errors
|
||||
|
||||
# Image handling
|
||||
cached_network_image: ^latest # Image caching
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Success Metrics
|
||||
|
||||
### UX Metrics
|
||||
|
||||
- **Time to complete common tasks**:
|
||||
- Change mosque name: < 30 seconds
|
||||
- Change location: < 2 minutes
|
||||
- Sync monthly data: < 1 minute
|
||||
- Update theme: < 45 seconds
|
||||
|
||||
- **Error rate**:
|
||||
- Form validation errors: < 5%
|
||||
- Navigation errors: < 1%
|
||||
- Focus loss: < 2%
|
||||
|
||||
- **User satisfaction**:
|
||||
- Task completion rate: > 95%
|
||||
- User-reported frustration: < 10%
|
||||
- Willingness to use again: > 90%
|
||||
|
||||
### Technical Metrics
|
||||
|
||||
- **Performance**:
|
||||
- Focus change: < 16ms (60fps)
|
||||
- Screen transition: < 200ms
|
||||
- Form submission: < 500ms
|
||||
- No frame drops during animations
|
||||
|
||||
- **Reliability**:
|
||||
- No crashes in 24h operation
|
||||
- No memory leaks
|
||||
- No focus loss bugs
|
||||
- Graceful error handling
|
||||
|
||||
---
|
||||
|
||||
## 17. Conclusion
|
||||
|
||||
Flutter is **fully capable** of building excellent TV admin dashboards. The key is:
|
||||
|
||||
1. **Embrace TV constraints** rather than fighting them
|
||||
2. **Think in focus**, not clicks
|
||||
3. **Minimize text input** through smart UI choices
|
||||
4. **Provide clear feedback** at every interaction
|
||||
5. **Test on real hardware** with actual users
|
||||
|
||||
The single-APK architecture is **totally viable** with these TV-optimized widgets. You don't need a separate phone app to deliver a great admin experience.
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. **Start with focus management** - this is the foundation
|
||||
2. **Build TV-optimized widgets** - large, clear, responsive
|
||||
3. **Reduce text input** - use selectors and wizards
|
||||
4. **Add polish** - sounds, animations, feedback
|
||||
5. **Test thoroughly** - on real TV with real users
|
||||
|
||||
Your admin dashboard will be excellent!
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Code Examples
|
||||
|
||||
### Complete TV TextField Example
|
||||
|
||||
See `/lib/ui/widgets/tv/tv_text_field.dart` for full implementation.
|
||||
|
||||
### Complete TV Selection Field Example
|
||||
|
||||
See `/lib/ui/widgets/tv/tv_selection_field.dart` for full implementation.
|
||||
|
||||
### Complete TV Dashboard Example
|
||||
|
||||
See `/lib/ui/screens/tv_admin_screen.dart` for full implementation.
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2025-01-XX
|
||||
**Author**: Claude Code
|
||||
**Status**: Ready for Implementation
|
||||
Reference in New Issue
Block a user