1

I'm developing an app which should work on both Android mobile devices as well as Android TVs. The app has issue focusing fields in login and signup forms. Here's minimal reproducible example:

import 'package:flutter/material.dart';

void main() => runApp(TestApp());

class TestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Test App'),
        ),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(8.0),
          child: TestWidget(),
        ),
      ),
    );
  }
}

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: List<Widget>.generate(5, (int index) => TextFormField(
        decoration: InputDecoration(
          labelText: 'Field ${index + 1}',
        ),
      )),
    );
  }
}

The focus moves to next field with Tab key but previous field is not getting focus when Shift+Tab is pressed. Also neither arrow keys nor Android TV remote D-PAD keys works for focus traversal of TextFormField but works fine for other focusable widgets.

rmalviya
  • 1,847
  • 12
  • 39

1 Answers1

1

It's a long open issue, but here we go:

Android TV support is to the date a bit shaky. But there are some easier (and some not so easy) workarounds to help us.

Basically start by reading this Medium article: Adding Android TV support to your Flutter App

The first important thing to do is to add support for the TV remote's Dpad by wrapping your ap in a Shortcut widget:

return Shortcuts(
      shortcuts: <LogicalKeySet, Intent>{
        LogicalKeySet(LogicalKeyboardKey.select): ActivateIntent(),
      },
      child: MaterialApp(
      ...
);

This supposedly forwards the select event from the keyboard to activate intent. ️

So far all your selectable stuff (buttons, etc.) should work. TextFormFields probably still won't, though.

Then go check this Flutter issue on GitHub: Focus gets lost on Android TV #49783

Hm... I don't know. It's a lengthy issue and I don't find an easy solution there, except for the RawKeyboardListener based approach. Which is (unintentionally) the way I kinda went.

When I started the endeavor a couple of days back, I didn't know about the Shortcut approach, so I looked at this package on pub.dev focusnode_widgets. I checked the code and understood, that the guy who did this, had a very specific problem at hand. So I stripped his example down and re-implemented his class _FocusNodeEnterTapActionableWidget (he really seem to like looooooooooong class names...).

Note: I won't paste this code here, because there is not that much difference, but what I did, I created a wrapper widget, that creates the desired widgets and is able to handle all necessary dpad events - including the ability to walk over TextFormFields.

In the following example _FocusableWidget is the replacement for _FocusNodeEnterTapActionableWidget.

/// get the type from a generic parameter
Type typeOf<T>() => T;

/// if you have large an scrollable pages with large gaps in between of focusable widgets
/// you need something to go to...
/// I used this to navigate through a help/about page
class FocusableAnchor extends StatelessWidget {
  @override
  Widget build(BuildContext context) => _FocusableWidget(
        autoFocus: false,
        handleEnterTapAction: (ctx) {},
        child: Container(),
        focusedBackgroundColor: Colors.transparent,
        nonFocusedBackgroundColor: Colors.transparent,
      );
}

/// there's another class I will leave out - it's just a wrapper for a given child
/// but I use a custom Android plugin to determine, whether I'm on a TV or not
/// TV: wrap the child widget, No TV: just use the child widget as-is.

/// this is the base wrapper, that is quite large, because it basically has all 
/// (some renamed) parameters, needed to create the following widgets:
/// IconButton, FloatingActionButton, MaterialButton, FlatButton, RaisedButton
/// DropdownButton (doesn't really work though, I don't understand why), ListTile
/// CheckboxListTile, RadioListTile (these both are shaky as well)
/// Switch, ActionChip, TextFormField
class FocusableValue<T extends Widget, U> extends StatelessWidget {
  FocusableValue({
    Key key,
    // the key of the widget to create
    this.widgetKey,
    // IconButton, FloatingActionButton
    this.icon, // DropdownButton
    this.onPressed,
    this.onLongPress,
    this.tooltip,
    // ... many more
    // explicitly set widget
    this.widget,
    this.colorFocused = true,
  }) : super(key: key);

  final Key widgetKey;

  final Widget icon;
  final void Function() onPressed;
  final void Function() onLongPress;
  final String tooltip;

  final T widget;

  final bool colorFocused;

  // a former colleague decided to use Kiwi for DI...
  final _uiMode = inject<UiModePlugin>();

  @override
  Widget build(BuildContext context) => FutureBuilder(
        future: _uiMode.getUiModeType(),
        initialData: null,
        builder: (ctx, snap) {
          if (!snap.hasData || snap.hasError || snap.data == null) {
            return Container();
          }
          var widget = this.widget;
          Function(BuildContext) onAction;
          Color focusBgColor;
          if (widget == null) {
            final type = typeOf<T>();
            switch (type) {
              case IconButton:
                widget = IconButton(
                  key: widgetKey,
                  icon: icon,
                  onPressed: onPressed,
                  tooltip: tooltip,
                  // ignore: avoid_as
                ) as T;
                onAction = (ctx) => onPressed();
                focusBgColor = colorAltDarkTransparent;
                break;
              // basically create your target here, e.g. TextFormField
              // TextFormField doesn't need to set the onAction function, though
            }
          }

          Decoration _decorate(Color color) {
            if (color == null) return null;
            return BoxDecoration(
              color: color,
              borderRadius: BorderRadius.circular(3.0),
              boxShadow: kElevationToShadow[1],
            );
          }

          EdgeInsetsGeometry _padding(bool apply) {
            if (!apply) return null;
            return EdgeInsets.only(
              top: 5.0,
              right: 5.0,
              bottom: 5.0,
              left: 5.0,
            );
          }

          BoxConstraints _constraints(bool apply) {
            if (!apply) return null;
            return BoxConstraints.tightFor(height: 37.0);
          }

          return UIMode.tv == snap.data
              ? _FocusableWidget(
                  autoFocus: false,
                  handleEnterTapAction: onAction ?? (ctx) {},
                  child: widget,
                  focusedBackgroundColor: null,
                  nonFocusedBackgroundColor: null,
                  focusedBackgroundDecoration:
                      colorFocused ? _decorate(focusBgColor) : null,
                  nonFocusedBackgroundDecoration: null,
                  padding: colorFocused ? _padding(focusBgColor != null) : null,
                  constraints:
                      colorFocused ? _constraints(focusBgColor != null) : null,
                )
              : widget;
        },
      );
}

/// for the most widgets, except for DropdownButton, RadioListTile I can use this wrapper
class Focusable<T extends Widget> extends FocusableValue<T, dynamic> {
  Focusable({
    Key key,
    Key widgetKey,
    Widget icon,
    Function() onPressed,
    Function() onLongPress,
    String tooltip,
    // more parameter to create other widgets
    T widget,
    bool colorFocused = true,
  }) : super(
          key: key,
          widgetKey: widgetKey,
          icon: icon,
          onPressed: onPressed,
          onLongPress: onLongPress,
          tooltip: tooltip,
          // pass all the other parameters here
          widget: widget,
          colorFocused: colorFocused,
        );
}

This is tedious, but in most of my cases it worked. I only still need to figure out CheckboxListTile and RadioListTile - they don't work within my (very heavily) customized dialog.

I hope, that helps a bit.

dzim
  • 1,131
  • 2
  • 12
  • 28