Files
jamshalat-masjid-screen/admin-ui-brief.md

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

  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

// 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:

  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):

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:

  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

// 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

// 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 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

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

  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

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