Mobile Development 8 min read

Implementing Android TV Apps with Flutter: Manifest Setup, Focus Management, and Remote Key Handling

This article explains how to configure AndroidManifest.xml for TV apps, use Focus and FocusableActionDetector to manage TabBar selection, handle remote control key events with KeyboardListener, and implement long‑press detection in Flutter for Android TV development.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Android TV Apps with Flutter: Manifest Setup, Focus Management, and Remote Key Handling

In Android development, to differentiate TV applications from phone applications, you need to declare a TV Activity in the AndroidManifest.xml file.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="flutter_tv_tmdb"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:...>
            ...
            <intent-filter>
                <action android:/>
                <category android:/>
                <category android:/>
            </intent-filter>
        </activity>
        ...
    </application>
    ...
</manifest>

Adding a <category android:/> inside the <intent-filter> triggers two errors in AndroidManifest.xml, which can be automatically resolved by the IDE, resulting in added <uses-feature android:/> entries and a android:banner attribute for the app’s adaptive icon.

The banner is the adaptive icon displayed on Android TV, following the official TV app icon design guidelines.

For TV apps, focus acquisition is crucial; an element can receive focus when it has an event handler, allowing selection via the remote’s directional keys.

To make a TabBar selectable, wrap each tab with a Focus or FocusableActionDetector and use the onFocusChange callback to animate the TabBarView :

TabBar(
  controller: controller,
  tabs: List.generate(
    tabs.length,
    (index) => FocusableActionDetector(
      focusNode: tabs[index].focusNode,
      autofocus: index == 0,
      onFocusChange: (focus) {
        controller.animateTo(index, duration: const Duration(milliseconds: 300), curve: Curves.ease);
      },
      child: Tab(text: tabs[index].text),
    ),
  ).toList(),
)

If you do not need an AppBar , place the TabBar inside the body ; otherwise, putting it in the AppBar title may limit onFocusChange to the first few tabs.

To capture remote key events, use the KeyboardListener widget and handle various keys in the onKeyEvent callback:

focusEventHandle(KeyEvent e) {
   switch (e.logicalKey) {
      case LogicalKeyboardKey.arrowRight:
        print('按了右键');
        break;
      case LogicalKeyboardKey.arrowLeft:
        print('按了左键');
        break;
      case LogicalKeyboardKey.arrowUp:
        print('按了上键');
        break;
      case LogicalKeyboardKey.arrowDown:
        print('按了下键');
        break;
      case LogicalKeyboardKey.select:
        print('按了确认键');
        break;
      case LogicalKeyboardKey.goBack:
        print('按了返回键');
        break;
      case LogicalKeyboardKey.contextMenu:
        print('按了菜单键');
        break;
      case LogicalKeyboardKey.audioVolumeUp:
        print('按了音量加键');
        break;
      case LogicalKeyboardKey.audioVolumeDown:
        print('按了音量减键');
        break;
      case LogicalKeyboardKey.f5:
        // Different brands may vary
        print('按了语音键');
        break;
    }
}

Note that not all remote controls have a menu key; however, the directional and select keys are universally present.

Each key press triggers both KeyDownEvent and KeyUpEvent , allowing detection of long‑press actions by measuring the time between these events:

late DateTime start;
void updateStart(DateTime time) {
  start = time;
  notifyListeners();
}

focusEventHandle(KeyEvent e) {
  if (e is KeyDownEvent) {
    updateStart(DateTime.now());
    print('按下了$start');
  }
  if (e is KeyUpEvent) {
    int duration = DateTime.now().difference(start).inMilliseconds;
    bool longPress = duration > 500;
    print('${DateTime.now()}');
    print('持续了$duration');
    if (longPress) {
      print('长按了');
    } else {
      print('没长按');
    }
  }
}

When a component has focus, the system automatically moves focus to it on directional input, so you only need to define the visual style for the selected state.

class ContentView extends StatelessWidget {
  const ContentView({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, provider, _) {
        List<HomeBtn> list = context.watch<AppProvider>().btnList;
        return Column(
          children: [
            TextButton(
              onPressed: () => context.read<AppProvider>().toTabView(context),
              child: const Text('下一个界面'),
            ),
            InkWell(onTap: () {}, child: const Text('这是自定义按钮')),
            Expanded(
              child: GridView.builder(
                itemCount: context.watch<AppProvider>().btnList.length,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  mainAxisSpacing: 12,
                  crossAxisSpacing: 12,
                  childAspectRatio: 16 / 9,
                ),
                itemBuilder: (context, index) {
                  HomeBtn btn = list[index];
                  return KeyboardListener(
                    focusNode: btn.focusNode,
                    onKeyEvent: (e) => context.read<AppProvider>().focusEventHandle(e, context, btn),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 120),
                      padding: EdgeInsets.all(btn.focusNode.hasFocus ? 6 : 12),
                      decoration: BoxDecoration(
                        color: list[index].focusNode.hasFocus
                            ? Colors.deepOrangeAccent.withOpacity(.5)
                            : Colors.white,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Image.asset(
                        list[index].image,
                        fit: BoxFit.cover,
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        );
      },
    );
  }
}

This concludes the key points for developing Android TV applications with Flutter.

Fluttermobile developmentAndroid TVManifestRemote ControlFocus Management
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.