21 KiB
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
- Always show focus: Users must see what's currently selected
- Predictable navigation: D-pad moves in expected directions
- Large focus targets: Everything must be easily selectable
- Clear visual feedback: Animated focus indicators
- Sound feedback: Audible confirmation of focus changes
Implementation Pattern
// 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.
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:
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:
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:
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:
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:
- Identitas - Mosque name, address
- Jadwal & Sync - Location, synchronization
- Tampilan - Theme, background, slideshow
- Rotasi - Screen timing
- Pembarisan - Updates, version
- Tentang - App information
6. Reducing Text Input (The Secret Sauce)
The best TV admin interfaces minimize typing. This is crucial because:
- TV remotes are terrible for text input
- On-screen keyboards are slow and error-prone
- Screen distance makes typing difficult
- User frustration increases with typing
Strategies to Minimize Typing
A. Use Selection Instead of Text Input
Bad (Text Input):
TextField(
decoration: InputDecoration(labelText: 'City ID API'),
)
Good (Selection):
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):
TVSearchSelectField<String>(
label: 'Cari Lokasi',
hint: 'Ketik nama kota...',
searchDelegate: MyQuranCitySearchDelegate(),
onSelect: (city) => saveCity(city),
)
C. Preset Options with Custom Value
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):
TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: 'Durasi Iqomah (menit)'),
)
Good (Slider):
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
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:
// 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
// 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
// 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:
- Natural flow: Left-to-right, top-to-bottom
- Wrap around: Last item → first item in same row
- Section boundaries: Clear visual separators
- Skip separators: Focus should jump over dividers
Keyboard Shortcuts
// 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
- Focus indicator: Border + background + shadow
- Pressed state: Scale down slightly (0.95x)
- Loading state: Progress indicator + "Loading..." text
- Success state: Green checkmark + success message
- Error state: Red X + error message
Sound Feedback
// 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:
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
// For game controllers with vibration
if (_focusNode.hasFocus) {
HapticFeedback.lightImpact(); // Subtle vibration on focus
}
10. Performance Considerations
Animation Performance
// 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
// Use ListView.builder for long lists
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return TVListItem(item: items[index]);
},
)
Image Optimization
// Cache images, use placeholders
CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => TVLoadingIndicator(),
errorWidget: (context, url, error) => TVErrorIcon(),
)
11. Accessibility
Screen Reader Support
// Add semantic labels
Semantics(
label: 'Nama masjid field',
value: settings.masjidName,
hint: 'Masukkan nama masjid',
child: TVTextField(...),
)
High Contrast Mode
// 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
// 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
TVFocusManagerwidget - Implement focus scope nodes
- Add focus listener utilities
- Create focus ring animation
- Test D-pad navigation
Scaling System
- Create
TVScalingutility - Implement responsive layout helpers
- Define typography scale
- Test on different screen sizes
Phase 2: Core Widgets (Week 2)
Form Widgets
TVTextFieldwith focus managementTVSelectionFieldwith dropdownTVButtonwith multiple stylesTVSwitchfor togglesTVSliderfor numeric values
Layout Widgets
TVDashboardTilefor grid itemsTVListItemfor list itemsTVCardfor containersTVSectionfor grouping
Phase 3: Advanced Features (Week 3)
Navigation & Layouts
- Grid-based dashboard screen
- Wizard-style multi-step form
- Tabbed interface
- Breadcrumb navigation
Input Optimization
TVSearchSelectFieldwith searchTVTimePickerfor time selectionTVDatePickerfor date selectionTVColorPickerfor 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
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
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
- Don't use small hit targets: Everything must be at least 48x48dp
- Don't rely on hover states: TV remotes don't have hover
- Don't use subtle gradients: Hard to see from distance
- Don't require extensive typing: It's frustrating on TV
- Don't use small fonts: Under 24px is unreadable from 2m
- Don't hide navigation: Always show clear focus indicators
- Don't use web-only patterns: TV ≠ web browser
- Don't forget sound feedback: Crucial for accessibility
- Don't ignore keyboard shortcuts: Power users expect them
- Don't make users backtrack: Forward-flow navigation only
✅ Do
- Do make focus obvious: Large borders, backgrounds, shadows
- Do provide clear feedback: Visual + audio + haptic
- Do minimize text input: Use selectors, wizards, presets
- Do use large fonts: Minimum 24px, recommended 28px+
- Do test on real TV: Emulator ≠ real experience
- Do optimize for D-pad: Natural navigation flow
- **Do add sound effects: Helpful feedback
- **Do use animations: Smooth transitions (200ms)
- **Do provide shortcuts: Keyboard navigation
- **Do think in steps: Break complex forms into wizards
15. Recommended Packages
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:
- Embrace TV constraints rather than fighting them
- Think in focus, not clicks
- Minimize text input through smart UI choices
- Provide clear feedback at every interaction
- 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
- Start with focus management - this is the foundation
- Build TV-optimized widgets - large, clear, responsive
- Reduce text input - use selectors and wizards
- Add polish - sounds, animations, feedback
- 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