coocaa_flutter_focus 0.2.0
coocaa_flutter_focus: ^0.2.0 copied to clipboard
A Flutter focus management library for TV, remote-control, and directional keyboard navigation.
coocaa_flutter_focus #
[coocaa_flutter_focus banner]
coocaa_flutter_focus is a Flutter focus management library for TV apps,
remote-control interaction, and directional keyboard navigation.
It provides a small public API around Flutter's FocusNode system, with
behavior tuned for large-screen interfaces: arrow-key traversal, focus groups,
focus memory, edge handling, focus-id lookup, and automatic scroll visibility.
Platform Support #
This package uses Flutter framework APIs only. It has no native plugin code and no platform-specific imports.
It is best suited for Android TV, large-screen Android apps, desktop keyboard apps, and web keyboard navigation. iOS is supported at the framework level when an external keyboard or directional input is available.
Features #
- Global directional focus coordination through
FocusController. - Focus registration and lifecycle handling through
FocusableWidget. - Logical focus areas through
FocusableGroup. - Group edge modes:
crossing,greedy, andblocked. - Group focus memory and
onBeforeFocusEnteroverrides. - Focus lookup and request by string
focusId. - Automatic scrolling for single taps and long-press directional movement.
- Configurable scroll edge offset and momentum through
FocusScrollConfig. - Back-key interception with newest-first callback order.
- Test coverage for traversal, nested groups, scrolling, long press, and cache invalidation.
Installation #
Add the package to your Flutter project:
dependencies:
coocaa_flutter_focus: ^0.2.0
For local development:
dependencies:
coocaa_flutter_focus:
path: ../coocaa_flutter_focus
Then import the public library entry:
import 'package:coocaa_flutter_focus/coocaa_flutter_focus.dart';
Quick Start #
Initialize the controller once near app startup. Use
FocusController.instance.navigatorKey if you want the controller to coordinate
with your app navigator.
Key setup
FocusController.instance ..init() ..updateConfig(scrollEdgeOffset: 80); MaterialApp( navigatorKey: FocusController.instance.navigatorKey, home: const FocusDemoPage(), );
import 'package:coocaa_flutter_focus/coocaa_flutter_focus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
FocusController.instance
..init()
..updateConfig(scrollEdgeOffset: 80);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: FocusController.instance.navigatorKey,
home: const FocusDemoPage(),
);
}
}
class FocusDemoPage extends StatelessWidget {
const FocusDemoPage({super.key});
@override
Widget build(BuildContext context) {
return FocusableGroup(
edgeFocusMode: FocusableGroupEdgeFocusMode.crossing,
child: Row(
children: List<Widget>.generate(4, (int index) {
return Padding(
padding: const EdgeInsets.all(8),
child: FocusableWidget(
focusId: 'card-$index',
autofocus: index == 0,
onEdge: (FocusNode node, LogicalKeyboardKey direction) {
debugPrint('Reached edge: $direction');
return null;
},
child: Builder(
builder: (BuildContext context) {
final bool focused = Focus.of(context).hasFocus;
return AnimatedContainer(
duration: const Duration(milliseconds: 120),
width: 160,
height: 96,
alignment: Alignment.center,
decoration: BoxDecoration(
color: focused ? Colors.blue : Colors.grey.shade700,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Card $index',
style: const TextStyle(color: Colors.white),
),
);
},
),
),
);
}),
),
);
}
}
Dispose the controller when the owning app or test surface is torn down:
@override
void dispose() {
FocusController.instance.dispose();
super.dispose();
}
FocusController #
FocusController.instance is the global coordinator.
Common methods:
init()registers keyboard, focus, and metrics listeners.dispose()removes listeners, stops pending scroll activity, and clears controller state.updateConfig(scrollConfig: ..., scrollEdgeOffset: ...)updates runtime scrolling behavior.setScrollConfig(config)updates onlyFocusScrollConfig.clearScrollEdgeOffset()removes the configured edge offset.isDirectionKey(key)returns true for arrow keys.findNextFocusNode(direction)resolves the next candidate without requesting focus.requestFocus(node, direction: ..., scrollable: true)requests focus and optionally scrolls the node into view.findFocusableById(id)returns a registered node byfocusId.requestFocusById(id, direction: ..., scrollable: true)requests focus byfocusId.addBackInterceptor(callback)intercepts back keys. It returns a remover callback.removeBackInterceptor(callback)removes a previously registered back interceptor.animateScrollPositionTo(position, target, ...)runs the same scroll animator used by focus movement for a specificScrollPosition.stopScrollPosition(position)stops an active focus scroll animation for a specificScrollPosition.
BackInterceptor callbacks run newest first. Return true to consume the
back key.
Low-level registration methods:
registerFocusable(...)andunregisterFocusable(node)are used byFocusableWidget. Call them directly only when building a custom focusable wrapper.registerGroup(...)andunregisterGroup(groupKey)are used byFocusableGroup. Call them directly only when building a custom group wrapper.
FocusableWidget #
Wrap every focusable item with FocusableWidget.
Important properties:
focusNode: provide your own node, or let the widget create one.autofocus: request initial focus through Flutter's focus system.canRequestFocusandskipTraversal: mirror standardFocusbehavior.autoScroll: allow or disable focus-driven scroll alignment for this item.focusId: register the node forfindFocusableByIdandrequestFocusById.debugLabel: label the internally created node.onKeyEvent: handle custom key events before default traversal.onFocusChange: observe focus changes.onDirection: override directional traversal. Return a target node to take over the move, return the same node to consume the repeat, or returnnullto use default traversal.onEdge: observe or override edge behavior.onInitNode: receive the effectiveFocusNode.
FocusableGroup #
Use FocusableGroup to model a row, panel, section, list, dialog, or any
logical focus area.
Important properties:
limitDirections: directions that may be constrained at this group's edge.edgeFocusMode: controls how traversal behaves when the group has no in-group target.memory: restore the last focused child when entering the group.onGroupFocusChange: reports group enter and leave state.onBeforeFocusEnter: choose a child before group memory or geometry wins.onEdge: observe or override group edge behavior.edgePadding: reserve extra viewport space for this group when scrolling.scrollCenter: center group targets when focus-driven scroll alignment runs.
Edge Modes #
FocusableGroupEdgeFocusMode.crossing: the default. Leaving the group prefers candidates that geometrically cross the current edge.FocusableGroupEdgeFocusMode.greedy: if no in-group candidate is found, continue to the nearest candidate on the requested side.FocusableGroupEdgeFocusMode.blocked: block configuredlimitDirections; unconfigured directions behave like greedy traversal.
Scroll Tuning #
Use FocusScrollConfig for single-tap and long-press scroll motion:
FocusController.instance.updateConfig(
scrollEdgeOffset: 96,
scrollConfig: const FocusScrollConfig(
singleTapDuration: Duration(milliseconds: 380),
singleTapMinDuration: Duration(milliseconds: 200),
singleTapVelocity: 1050,
singleTapRetargetVelocity: 3200,
longPressStartDelay: Duration(milliseconds: 180),
longPressAccelerationDuration: Duration(milliseconds: 220),
longPressMaxVelocity: 3600,
),
);
scrollEdgeOffset keeps focused content away from the viewport edge. Group
edgePadding can further constrain focus scroll behavior inside nested or
large content sections.
Testing #
Widget tests should initialize and dispose the controller explicitly:
void main() {
setUp(() {
FocusController.instance.init();
});
tearDown(() {
FocusController.instance.dispose();
});
}
Run static analysis:
flutter analyze
Run tests:
flutter test
Exported API #
Import only the public library entry:
import 'package:coocaa_flutter_focus/coocaa_flutter_focus.dart';
The package exports:
FocusControllerFocusScrollConfigFocusableWidgetFocusableGroupFocusableGroupEdgeFocusModeFocusDirectionCallbackFocusEdgeCallbackGroupFocusChangeCallbackGroupBeforeFocusEnterCallbackBackInterceptor
Contact #
For questions or feedback, contact wuronghua@coocaa.com.