coocaa_flutter_focus 0.2.0 copy "coocaa_flutter_focus: ^0.2.0" to clipboard
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 #

Version Flutter Dart Input Tests License: MIT

[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, and blocked.
  • Group focus memory and onBeforeFocusEnter overrides.
  • 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 only FocusScrollConfig.
  • 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 by focusId.
  • requestFocusById(id, direction: ..., scrollable: true) requests focus by focusId.
  • 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 specific ScrollPosition.
  • stopScrollPosition(position) stops an active focus scroll animation for a specific ScrollPosition.

BackInterceptor callbacks run newest first. Return true to consume the back key.

Low-level registration methods:

  • registerFocusable(...) and unregisterFocusable(node) are used by FocusableWidget. Call them directly only when building a custom focusable wrapper.
  • registerGroup(...) and unregisterGroup(groupKey) are used by FocusableGroup. 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.
  • canRequestFocus and skipTraversal: mirror standard Focus behavior.
  • autoScroll: allow or disable focus-driven scroll alignment for this item.
  • focusId: register the node for findFocusableById and requestFocusById.
  • 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 return null to use default traversal.
  • onEdge: observe or override edge behavior.
  • onInitNode: receive the effective FocusNode.

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 configured limitDirections; 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:

  • FocusController
  • FocusScrollConfig
  • FocusableWidget
  • FocusableGroup
  • FocusableGroupEdgeFocusMode
  • FocusDirectionCallback
  • FocusEdgeCallback
  • GroupFocusChangeCallback
  • GroupBeforeFocusEnterCallback
  • BackInterceptor

Contact #

For questions or feedback, contact wuronghua@coocaa.com.

1
likes
0
points
73
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

A Flutter focus management library for TV, remote-control, and directional keyboard navigation.

Repository
View/report issues

Topics

#flutter #focus #tv #keyboard #remote-control

License

unknown (license)

Dependencies

flutter

More

Packages that depend on coocaa_flutter_focus