focus_tree_inspector 0.1.0
focus_tree_inspector: ^0.1.0 copied to clipboard
DevTools extension and bridge for best-effort Flutter focus tree inspection.
import 'package:flutter/material.dart';
import 'package:focus_tree_inspector/focus_tree_inspector.dart';
import 'package:focus_tree_example/src/widgets/focus_demo_widgets.dart';
void main() {
FocusTreeInspector.instance.registerFocusTreeBridge();
runApp(const MyApp());
}
final class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Focus Tree Example',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
home: const FocusPlaygroundPage(),
);
}
}
final class FocusPlaygroundPage extends StatefulWidget {
const FocusPlaygroundPage({super.key});
@override
State<FocusPlaygroundPage> createState() => _FocusPlaygroundPageState();
}
final class _FocusPlaygroundPageState extends State<FocusPlaygroundPage> {
late final TextEditingController _textController;
late final FocusNode _textFieldFocusNode;
@override
void initState() {
super.initState();
_textController = TextEditingController(text: 'Tap tiles in DevTools to focus');
_textFieldFocusNode = FocusNode(debugLabel: 'TextField');
}
@override
void dispose() {
_textController.dispose();
_textFieldFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Focus playground'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const FocusDemoSectionTitle('SingleChildScrollView (this page)'),
const SizedBox(height: 12),
FocusDemoSection(
title: 'TextField (focusNode + keyboard)',
child: TextField(
controller: _textController,
focusNode: _textFieldFocusNode,
decoration: const InputDecoration(border: OutlineInputBorder(), labelText: 'TextField'),
),
),
FocusDemoSection(
title: 'Horizontal ListView.builder (5 focusable tiles)',
child: SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 5,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 12),
child: SizedBox(
width: 96,
child: FocusSquareTile(debugLabel: 'HList tile $index', label: '$index', colorSeed: index),
),
);
},
),
),
),
FocusDemoSection(
title: 'GridView.builder (5 focusable tiles)',
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 5,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1,
),
itemBuilder: (context, index) {
return FocusSquareTile(debugLabel: 'Grid tile $index', label: '$index', colorSeed: index + 10);
},
),
),
FocusDemoSection(
title: 'Buttons in a row',
child: Row(
children: const [
Expanded(
child: FocusLabeledButton(debugLabel: 'Row button 0', label: 'Row 0'),
),
SizedBox(width: 12),
Expanded(
child: FocusLabeledButton(debugLabel: 'Row button 1', label: 'Row 1'),
),
SizedBox(width: 12),
Expanded(
child: FocusLabeledButton(debugLabel: 'Row button 2', label: 'Row 2'),
),
],
),
),
FocusDemoSection(
title: 'Buttons in a column',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
FocusLabeledButton(debugLabel: 'Column button 0', label: 'Column 0'),
SizedBox(height: 12),
FocusLabeledButton(debugLabel: 'Column button 1', label: 'Column 1'),
SizedBox(height: 12),
FocusLabeledButton(debugLabel: 'Column button 2', label: 'Column 2'),
],
),
),
const FocusDemoSection(
title: 'Multiple FocusScopes + FocusTraversalGroups (for indexing)',
child: MultiScopeTraversalPlayground(),
),
FocusDemoSection(
title: 'FocusTraversalGroup + custom order',
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: const [
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(3),
child: FocusSquareTile(debugLabel: 'Ordered tile 3', label: '3', colorSeed: 30),
),
),
SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(1),
child: FocusSquareTile(debugLabel: 'Ordered tile 1', label: '1', colorSeed: 31),
),
),
SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(2),
child: FocusSquareTile(debugLabel: 'Ordered tile 2', label: '2', colorSeed: 32),
),
),
],
),
),
),
FocusDemoSection(
title: 'Non-focusable / skip traversal variants',
child: Row(
children: const [
Expanded(
child: FocusSquareTile(
debugLabel: 'canRequestFocus=false',
label: 'noReq',
colorSeed: 40,
canRequestFocus: false,
),
),
SizedBox(width: 12),
Expanded(
child: FocusSquareTile(
debugLabel: 'skipTraversal=true',
label: 'skip',
colorSeed: 41,
skipTraversal: true,
),
),
SizedBox(width: 12),
Expanded(
child: ExcludeFocus(
child: FocusSquareTile(debugLabel: 'ExcludeFocus', label: 'excluded', colorSeed: 42),
),
),
],
),
),
FocusDemoSection(
title: 'Autofocus tile (starts focused)',
child: const Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 120,
child: FocusSquareTile(debugLabel: 'autofocus tile', label: 'auto', colorSeed: 50),
),
),
),
FocusDemoSection(
title: 'Navigation',
child: Align(
alignment: Alignment.centerLeft,
child: FocusActionButton(
debugLabel: 'Open next page',
label: 'Open next page',
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const NextPage()));
},
),
),
),
const SizedBox(height: 24),
ElevatedButton(onPressed: debugDumpFocusTree, child: const Text('debugDumpFocusTree()')),
],
),
),
),
);
}
}
final class MultiScopeTraversalPlayground extends StatelessWidget {
const MultiScopeTraversalPlayground({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Use this section to validate scopeTraversalIndex (local) vs traversalIndex (global). '
'Each card is an explicit FocusScope with its own traversal groups.',
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: LabeledFocusScopeCard(
scopeDebugLabel: 'Scope A',
title: 'Scope A (ReadingOrder, with nested Ordered group)',
child: FocusTraversalGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 92,
child: Row(
children: [
Expanded(
child: FocusSquareTile(debugLabel: 'A tile 0', label: 'A0', colorSeed: 70),
),
SizedBox(width: 12),
Expanded(
child: FocusSquareTile(debugLabel: 'A tile 1', label: 'A1', colorSeed: 71),
),
],
),
),
const SizedBox(height: 12),
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: [
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(20),
child: FocusSquareTile(debugLabel: 'A ordered 20', label: '20', colorSeed: 72),
),
),
const SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(10),
child: FocusSquareTile(debugLabel: 'A ordered 10', label: '10', colorSeed: 73),
),
),
const SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(30),
child: FocusSquareTile(debugLabel: 'A ordered 30', label: '30', colorSeed: 74),
),
),
],
),
),
const SizedBox(height: 12),
FocusLabeledButton(debugLabel: 'A button', label: 'A button'),
],
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: LabeledFocusScopeCard(
scopeDebugLabel: 'Scope B',
title: 'Scope B (OrderedTraversalPolicy at scope root)',
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: FocusLabeledButton(debugLabel: 'B button 2', label: 'B 2'),
),
const SizedBox(height: 12),
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: FocusLabeledButton(debugLabel: 'B button 1', label: 'B 1'),
),
const SizedBox(height: 12),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: FocusTraversalGroup(
child: Row(
children: [
Expanded(
child: FocusSquareTile(debugLabel: 'B tile 0', label: 'B0', colorSeed: 75),
),
const SizedBox(width: 12),
Expanded(
child: FocusSquareTile(debugLabel: 'B tile 1', label: 'B1', colorSeed: 76),
),
],
),
),
),
const SizedBox(height: 12),
ExcludeFocus(
child: FocusSquareTile(debugLabel: 'B excluded', label: 'X', colorSeed: 77),
),
],
),
),
),
),
],
),
],
);
}
}
final class NextPage extends StatelessWidget {
const NextPage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Next page'),
automaticallyImplyLeading: false,
leading: const BackButton(),
bottom: const TabBar(
tabs: [
Tab(text: 'Form'),
Tab(text: 'Scroll'),
],
),
),
body: const TabBarView(children: [NextPageFormTab(), NextPageScrollTab()]),
),
);
}
}
final class NextPageFormTab extends StatelessWidget {
const NextPageFormTab({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'This page is designed to stress focus traversal: nested scopes, nested traversal groups, '
'and mixed widget types.',
),
const SizedBox(height: 12),
LabeledFocusScopeCard(
scopeDebugLabel: 'NextPage Scope 1',
title: 'Scope 1 (ReadingOrder + nested Ordered group)',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const DebugTextField(debugLabel: 'NP field A', labelText: 'Field A'),
const SizedBox(height: 12),
const DebugTextField(debugLabel: 'NP field B', labelText: 'Field B'),
const SizedBox(height: 12),
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: [
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(2),
child: const FocusSquareTile(debugLabel: 'NP ordered 2', label: '2', colorSeed: 81),
),
),
const SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(1),
child: const FocusSquareTile(debugLabel: 'NP ordered 1', label: '1', colorSeed: 82),
),
),
const SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(3),
child: const FocusSquareTile(debugLabel: 'NP ordered 3', label: '3', colorSeed: 83),
),
),
],
),
),
],
),
),
const SizedBox(height: 12),
LabeledFocusScopeCard(
scopeDebugLabel: 'NextPage Scope 2',
title: 'Scope 2 (OrderedTraversalPolicy over mixed controls)',
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FocusTraversalOrder(
order: NumericFocusOrder(10),
child: const DebugTextField(debugLabel: 'NP field C', labelText: 'Field C'),
),
const SizedBox(height: 12),
FocusTraversalOrder(
order: NumericFocusOrder(20),
child: const DebugSwitchTile(debugLabel: 'NP switch', title: 'Enable option'),
),
const SizedBox(height: 12),
FocusTraversalOrder(
order: NumericFocusOrder(30),
child: const DebugSlider(debugLabel: 'NP slider', label: 'Intensity'),
),
],
),
),
),
const SizedBox(height: 24),
ElevatedButton(onPressed: debugDumpFocusTree, child: const Text('debugDumpFocusTree()')),
],
),
),
);
}
}
final class NextPageScrollTab extends StatelessWidget {
const NextPageScrollTab({super.key});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Nested scroll + traversal groups (try tabbing through list + grid)'),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FocusTraversalGroup(
child: SizedBox(
height: 88,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: 8,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) {
return SizedBox(
width: 80,
child: FocusSquareTile(debugLabel: 'NP HList $index', label: 'H$index', colorSeed: 90 + index),
);
},
),
),
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.separated(
itemCount: 10,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
return FocusListRow(
debugLabel: 'NP list row $index',
title: 'Row $index',
subtitle: 'List item with its own FocusNode',
);
},
),
),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: const [
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(2),
child: FocusSquareTile(debugLabel: 'NP footer ordered 2', label: 'F2', colorSeed: 120),
),
),
SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(1),
child: FocusSquareTile(debugLabel: 'NP footer ordered 1', label: 'F1', colorSeed: 121),
),
),
SizedBox(width: 12),
Expanded(
child: FocusTraversalOrder(
order: NumericFocusOrder(3),
child: FocusSquareTile(debugLabel: 'NP footer ordered 3', label: 'F3', colorSeed: 122),
),
),
],
),
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 24)),
],
);
}
}