- 🎯 Simple Setup: Just 3 steps to get started
- 🎨 Customizable Effects: Built-in focus effects + custom builders
- 📺 Platform Support: Android TV, Fire TV, Apple TV, and more
- ⚡ Performance: Optimized for smooth navigation
- 🔧 Programmatic Control: Full API for programmatic navigation
- 🎮 Game Controller Support: Works with standard controllers
- 🔄 Sequential Navigation: Previous/Next support for media and lists
dependencies:
dpad: anyimport 'package:dpad/dpad.dart';
void main() {
runApp(
DpadNavigator(
enabled: true,
child: MaterialApp(
home: MyApp(),
),
),
);
}class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
DpadFocusable(
autofocus: true,
onFocus: () => print('Focused'),
onSelect: () => print('Selected'),
builder: (context, isFocused, child) {
return AnimatedContainer(
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
border: Border.all(
color: isFocused ? Colors.blue : Colors.transparent,
width: 3,
),
borderRadius: BorderRadius.circular(8),
),
child: child,
);
},
child: ElevatedButton(
onPressed: () => print('Pressed'),
child: Text('Button 1'),
),
),
DpadFocusable(
onSelect: () => print('Button 2 selected'),
child: ElevatedButton(
onPressed: () => print('Pressed'),
child: Text('Button 2'),
),
),
],
);
}
}// Border highlight
DpadFocusable(
builder: FocusEffects.border(color: Colors.blue),
child: MyWidget(),
)
// Glow effect
DpadFocusable(
builder: FocusEffects.glow(glowColor: Colors.blue),
child: MyWidget(),
)
// Scale effect
DpadFocusable(
builder: FocusEffects.scale(scale: 1.1),
child: MyWidget(),
)
// Gradient background
DpadFocusable(
builder: FocusEffects.gradient(
focusedColors: [Colors.blue, Colors.purple],
),
child: MyWidget(),
)
// Combine multiple effects
DpadFocusable(
builder: FocusEffects.combine([
FocusEffects.scale(scale: 1.05),
FocusEffects.border(color: Colors.blue),
]),
child: MyWidget(),
)DpadFocusable(
builder: (context, isFocused, child) {
return Transform.scale(
scale: isFocused ? 1.1 : 1.0,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration: BoxDecoration(
boxShadow: isFocused ? [
BoxShadow(
color: Colors.blue.withValues(alpha: 0.6), // ignore: deprecated_member_use
blurRadius: 20,
spreadRadius: 2,
),
] : null,
),
child: child,
),
);
},
child: Container(
child: Text('Custom Effect'),
),
)DpadFocusable now automatically scrolls to ensure the focused widget is fully visible, including focus effects like glow and borders.
DpadFocusable(
autoScroll: true, // Enable auto-scroll (default: true)
scrollPadding: 24.0, // Extra padding for focus effects (default: 24.0)
builder: FocusEffects.glow(glowColor: Colors.blue),
child: MyWidget(),
)
// Disable auto-scroll for specific widgets
DpadFocusable(
autoScroll: false,
child: MyWidget(),
)
// Programmatic scroll control
Dpad.scrollToFocus(
focusNode,
padding: 32.0,
duration: Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);The focus memory system intelligently remembers user's focus positions and restores them when navigating back, providing a more natural TV navigation experience.
DpadNavigator(
focusMemory: FocusMemoryOptions(
enabled: true,
maxHistory: 20,
),
onNavigateBack: (context, previousEntry, history) {
if (previousEntry != null) {
previousEntry.focusNode.requestFocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: MyApp(),
)// Tab bar
DpadFocusable(
region: 'tabs',
child: TabButton(),
)
// Filter area
DpadFocusable(
region: 'filters',
child: FilterOption(),
)
// Content cards
DpadFocusable(
region: 'cards',
child: ContentCard(),
)- Tab Navigation: Tab A → Browse → Tab B → Back → Tab B (restores previous tab)
- Filter Navigation: Filter A → Browse → Filter A → Back → Filter A (restores previous filter)
- Cross-Route Navigation: Maintains separate focus history per route
Region-based navigation solves the common TV UX problem where Flutter's default geometric navigation doesn't match user expectations.
With default Flutter navigation:
- Tab → Content: might focus any card based on distance
- Content → Tab: might jump to unexpected tab
- Sidebar → Grid: focus could land anywhere
DpadNavigator(
regionNavigation: RegionNavigationOptions(
enabled: true,
rules: [
// Tab → Content: always focus first card
RegionNavigationRule(
fromRegion: 'tabs',
toRegion: 'content',
direction: TraversalDirection.down,
strategy: RegionNavigationStrategy.fixedEntry,
bidirectional: true,
reverseStrategy: RegionNavigationStrategy.memory,
),
// Sidebar → Grid: always focus first card
RegionNavigationRule(
fromRegion: 'sidebar',
toRegion: 'grid',
direction: TraversalDirection.right,
strategy: RegionNavigationStrategy.fixedEntry,
),
],
),
child: MyApp(),
)// First card in content area - entry point
DpadFocusable(
region: 'content',
isEntryPoint: true,
child: ContentCard(),
)
// Other cards in the same region
DpadFocusable(
region: 'content',
child: ContentCard(),
)| Strategy | Behavior |
|---|---|
geometric |
Flutter's default distance-based navigation |
fixedEntry |
Always focus the widget marked as isEntryPoint |
memory |
Restore last focused widget, fallback to entry point |
custom |
Use custom resolver function |
DpadNavigator(
customShortcuts: {
LogicalKeyboardKey.keyG: () => _showGridView(),
LogicalKeyboardKey.keyL: () => _showListView(),
LogicalKeyboardKey.keyR: () => _refreshData(),
LogicalKeyboardKey.keyS: () => _showSearch(),
},
onMenuPressed: () => _showMenu(),
onBackPressed: () => _handleBack(),
child: MyApp(),
)Default Keyboard Shortcuts (v1.1.0+):
- Arrow Keys: Directional navigation (up, down, left, right)
- Tab/Shift+Tab: Sequential navigation (next/previous)
- Media Track Next/Previous: Media control navigation
- Channel Up/Down: TV remote sequential navigation
- Enter/Select/Space: Trigger selection action
- Escape/Back: Navigate back
- ContextMenu: Show menu
// Navigate in directions
Dpad.navigateUp(context);
Dpad.navigateDown(context);
Dpad.navigateLeft(context);
Dpad.navigateRight(context);
// Sequential navigation (new in v1.1.0)
Dpad.navigateNext(context); // Tab / Media Track Next
Dpad.navigatePrevious(context); // Shift+Tab / Media Track Previous
// Focus management
final currentFocus = Dpad.currentFocus;
Dpad.requestFocus(myFocusNode);
Dpad.clearFocus();DpadNavigator(
onMenuPressed: () {
// Handle menu button on TV remotes
_showMenu();
},
onBackPressed: () {
// Handle back button
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
child: MyApp(),
)- Android TV: Full native D-pad support
- Amazon Fire TV: Compatible with Fire TV remotes
- Apple TV: Works with Siri Remote (Flutter web)
- Game Controllers: Standard controller navigation
- Generic TV Platforms: Any D-pad compatible input
- Always set
autofocus: trueon one widget per screen for initial focus - Test with real D-pad hardware, not just keyboard arrows
- Consider focus order - arrange widgets logically for navigation
- Provide clear visual feedback - use prominent focus indicators
- Handle edge cases - what happens when navigation fails?
The system consists of three main components:
- DpadNavigator: Root widget that captures D-pad events
- DpadFocusable: Wrapper that makes widgets focusable
- Dpad: Utility class for programmatic control
All components work together seamlessly with Flutter's focus system.
Coming from other TV navigation libraries?
- ✅ No complex configuration needed
- ✅ Works with standard Flutter widgets
- ✅ No custom FocusNode management required
- ✅ Built-in support for all TV platforms
- ✅ Extensive customization options
Check out the example app for a complete implementation showing:
- Grid navigation
- List navigation
- Custom focus effects
- Programmatic navigation
- Platform-specific handling
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
