# 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 createState() => _TVWidgetState(); } class _TVWidgetState extends State { 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( 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( 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( label: 'Cari Lokasi', hint: 'Ketik nama kota...', searchDelegate: MyQuranCitySearchDelegate(), onSelect: (city) => saveCity(city), ) ``` #### C. Preset Options with Custom Value ```dart TVSelectionField( 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 playFocus() => _audioPlayer.play(AssetSource('sounds/focus.mp3')); Future playSelect() => _audioPlayer.play(AssetSource('sounds/select.mp3')); Future playError() => _audioPlayer.play(AssetSource('sounds/error.mp3')); Future 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