### Run Flutter Demo on Chrome Source: https://github.com/google/flutter.widgets/blob/master/packages/self_storing_input/example/readme.md Use this command to run the Flutter demo application on the Chrome browser. Ensure you have Flutter installed and configured. ```bash flutter run -d chrome ``` -------------------------------- ### Creating a New Flutter Project Source: https://github.com/google/flutter.widgets/blob/master/packages/visibility_detector/README.md This shell command sequence is used to create a new default Flutter project within an existing directory, typically for running example applications. ```shell cd example flutter create . ``` -------------------------------- ### ScrollablePositionedList with Control Buttons Source: https://context7.com/google/flutter.widgets/llms.txt This example shows how to integrate ScrollablePositionedList.builder with buttons to control scrolling. It requires ItemScrollController, ScrollOffsetController, and ItemPositionsListener for managing list state and navigation. ```dart import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class ChatMessageList extends StatefulWidget { @override _ChatMessageListState createState() => _ChatMessageListState(); } class _ChatMessageListState extends State { final ItemScrollController itemScrollController = ItemScrollController(); final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); final List messages = List.generate(1000, (i) => 'Message $i'); @override Widget build(BuildContext context) { return Column( children: [ // Control buttons Padding( padding: EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () => itemScrollController.jumpTo(index: 0), child: Text('Jump to Start'), ), ElevatedButton( onPressed: () => itemScrollController.scrollTo( index: 500, duration: Duration(seconds: 2), curve: Curves.easeInOutCubic, ), child: Text('Scroll to #500'), ), ElevatedButton( onPressed: () => itemScrollController.jumpTo( index: messages.length - 1, alignment: 1.0, ), child: Text('Jump to End'), ), ], ), ), // Message list Expanded( child: ScrollablePositionedList.builder( itemCount: messages.length, itemScrollController: itemScrollController, scrollOffsetController: scrollOffsetController, itemPositionsListener: itemPositionsListener, itemBuilder: (context, index) => Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: ListTile( leading: CircleAvatar(child: Text('${index % 10}')), title: Text(messages[index]), subtitle: Text('Sent at ${index}:00'), ), ), ), ), ], ); } } ``` -------------------------------- ### Initialize ScrollablePositionedList Source: https://github.com/google/flutter.widgets/blob/master/packages/scrollable_positioned_list/README.md Create a ScrollablePositionedList using controllers and listeners to manage scrolling and visibility tracking. ```dart final ItemScrollController itemScrollController = ItemScrollController(); final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create() ScrollablePositionedList.builder( itemCount: 500, itemBuilder: (context, index) => Text('Item $index'), itemScrollController: itemScrollController, scrollOffsetController: scrollOffsetController, itemPositionsListener: itemPositionsListener, scrollOffsetListener: scrollOffsetListener, ); ``` -------------------------------- ### Define OverlayController Source: https://github.com/google/flutter.widgets/blob/master/packages/self_storing_input/README.md Initialize an OverlayController within the screen state to manage input overlays. ```dart OverlayController _controller = OverlayController(); ``` -------------------------------- ### Configure SelfStoringText Widget Source: https://github.com/google/flutter.widgets/blob/master/packages/self_storing_input/README.md Pass the OverlayController instance to the SelfStoringText widget to enable overlay management. ```dart SelfStoringText( overlayController: _controller, ... ``` -------------------------------- ### Implement the Saver interface Source: https://context7.com/google/flutter.widgets/llms.txt Define custom loading, validation, and saving logic by implementing the Saver interface. Use the itemKey to identify specific data fields. ```dart import 'package:self_storing_input/self_storing_input.dart'; class ApiSaver implements Saver { final Map _cache = {}; @override Future load(String itemKey) async { // Simulate API call await Future.delayed(Duration(milliseconds: 200)); return _cache[itemKey] as T?; } @override OperationResult validate(String itemKey, T? value) { // Synchronous validation if (itemKey == 'email' && value is String) { if (!value.contains('@')) { return OperationResult.error('Invalid email format'); } } if (itemKey == 'age' && value is String) { final age = int.tryParse(value); if (age == null || age < 0 || age > 150) { return OperationResult.error('Age must be between 0 and 150'); } } return OperationResult.success(); } @override Future save(String itemKey, T? value) async { try { // Simulate API call await Future.delayed(Duration(milliseconds: 500)); _cache[itemKey] = value; return OperationResult.success(); } catch (e) { return OperationResult.error('Failed to save: $e'); } } } ``` -------------------------------- ### Control tree expansion with TreeController Source: https://context7.com/google/flutter.widgets/llms.txt Manage the expand and collapse state of tree nodes programmatically using a TreeController instance. ```dart import 'package:flutter/material.dart'; import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; class ControlledTreeExample extends StatefulWidget { @override _ControlledTreeExampleState createState() => _ControlledTreeExampleState(); } class _ControlledTreeExampleState extends State { final Key nodeKey = ValueKey('important-node'); final TreeController _controller = TreeController(allNodesExpanded: false); @override Widget build(BuildContext context) { return Column( children: [ Row( children: [ ElevatedButton( onPressed: () => setState(() => _controller.expandAll()), child: Text('Expand All'), ), SizedBox(width: 8), ElevatedButton( onPressed: () => setState(() => _controller.collapseAll()), child: Text('Collapse All'), ), SizedBox(width: 8), ElevatedButton( onPressed: () => setState(() => _controller.expandNode(nodeKey)), child: Text('Expand Node'), ), SizedBox(width: 8), ElevatedButton( onPressed: () => setState(() => _controller.collapseNode(nodeKey)), child: Text('Collapse Node'), ), ], ), SizedBox(height: 16), Expanded( child: TreeView( treeController: _controller, nodes: [ TreeNode( content: Text('Root'), key: nodeKey, children: [ TreeNode(content: Text('Child 1')), TreeNode(content: Text('Child 2')), TreeNode( content: Text('Child 3'), children: [ TreeNode(content: Text('Grandchild')), ], ), ], ), ], ), ), ], ); } } ``` -------------------------------- ### Basic VisibilityDetector Usage Source: https://github.com/google/flutter.widgets/blob/master/packages/visibility_detector/README.md Wrap a widget with VisibilityDetector to receive callbacks when its visibility changes. The callback provides the visibility fraction and the widget's key. ```dart Widget build(BuildContext context) { return VisibilityDetector( key: Key('my-widget-key'), onVisibilityChanged: (visibilityInfo) { var visiblePercentage = visibilityInfo.visibleFraction * 100; debugPrint( 'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible'); }, child: someOtherWidget, ); } ``` -------------------------------- ### Use the SelfStoringText widget Source: https://context7.com/google/flutter.widgets/llms.txt Integrate the SelfStoringText widget into a form to enable automatic saving and validation. Requires an OverlayController and a Saver implementation. ```dart import 'package:flutter/material.dart'; import 'package:self_storing_input/self_storing_input.dart'; class UserProfileForm extends StatefulWidget { @override _UserProfileFormState createState() => _UserProfileFormState(); } class _UserProfileFormState extends State { final OverlayController _overlayController = OverlayController(); final ApiSaver _saver = ApiSaver(); @override Widget build(BuildContext context) { return GestureDetector( // Close editing overlays on tap outside onTap: () => _overlayController.close(), child: Scaffold( appBar: AppBar(title: Text('Edit Profile')), body: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Name:', style: TextStyle(fontWeight: FontWeight.bold)), SelfStoringText( 'user_name', saver: _saver, overlayController: _overlayController, emptyText: 'Click to add name', style: SelfStoringTextStyle( textStyle: TextStyle(fontSize: 16), ), ), SizedBox(height: 24), Text('Bio:', style: TextStyle(fontWeight: FontWeight.bold)), SelfStoringText( 'user_bio', saver: _saver, overlayController: _overlayController, emptyText: 'Click to add bio', style: SelfStoringTextStyle( overlayStyle: OverlayStyle.forTextEditor(height: 150), keyboardType: TextInputType.multiline, maxLines: null, ), ), ], ), ), ), ); } } ``` -------------------------------- ### Track Visible Items with ItemPositionsListener Source: https://context7.com/google/flutter.widgets/llms.txt Use ItemPositionsListener to monitor which items are currently visible in the viewport. Each ItemPosition provides the item's index and its leading/trailing edge positions as fractions of the viewport. Requires importing 'package:scrollable_positioned_list/scrollable_positioned_list.dart'. ```dart import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class VisibleItemsTracker extends StatefulWidget { @override _VisibleItemsTrackerState createState() => _VisibleItemsTrackerState(); } class _VisibleItemsTrackerState extends State { final ItemScrollController itemScrollController = ItemScrollController(); final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); @override Widget build(BuildContext context) { return Column( children: [ // Display visible items info ValueListenableBuilder>( valueListenable: itemPositionsListener.itemPositions, builder: (context, positions, child) { if (positions.isEmpty) { return Text('No items visible'); } // Find first and last visible items final firstVisible = positions .where((pos) => pos.itemTrailingEdge > 0) .reduce((a, b) => a.itemTrailingEdge < b.itemTrailingEdge ? a : b); final lastVisible = positions .where((pos) => pos.itemLeadingEdge < 1) .reduce((a, b) => a.itemLeadingEdge > b.itemLeadingEdge ? a : b); return Container( padding: EdgeInsets.all(16), color: Colors.blue[100], child: Column( children: [ Text('First visible: Item ${firstVisible.index}'), Text('Last visible: Item ${lastVisible.index}'), Text('Total visible: ${positions.length} items'), ], ), ); }, ), // Scrollable list Expanded( child: ScrollablePositionedList.builder( itemCount: 500, itemScrollController: itemScrollController, itemPositionsListener: itemPositionsListener, itemBuilder: (context, index) => Container( height: 80, margin: EdgeInsets.all(4), color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3), alignment: Alignment.center, child: Text('Item $index', style: TextStyle(fontSize: 18)), ), ), ), ], ); } } ``` -------------------------------- ### Configure Visibility Detection with VisibilityDetectorController Source: https://context7.com/google/flutter.widgets/llms.txt Customize the update interval for visibility detection using VisibilityDetectorController. For testing purposes, set the update interval to Duration.zero for immediate updates. Ensure pending callbacks are notified before disposing of the controller. ```dart import 'package:flutter/material.dart'; import 'package:visibility_detector/visibility_detector.dart'; class VisibilityControllerExample extends StatefulWidget { @override _VisibilityControllerExampleState createState() => _VisibilityControllerExampleState(); } class _VisibilityControllerExampleState extends State { String _status = 'Unknown'; double _visibleFraction = 0; @override void initState() { super.initState(); // Set update interval (default is 500ms) // For tests, use Duration.zero for immediate updates VisibilityDetectorController.instance.updateInterval = Duration(milliseconds: 100); } @override void dispose() { // Force pending callbacks before disposal VisibilityDetectorController.instance.notifyNow(); super.dispose(); } @override Widget build(BuildContext context) { return Column( children: [ Container( padding: EdgeInsets.all(16), color: Colors.amber[100], child: Column( children: [ Text('Status: $_status'), Text('Visible: ${(_visibleFraction * 100).toStringAsFixed(1)}%'), LinearProgressIndicator(value: _visibleFraction), ], ), ), Expanded( child: ListView( children: [ Container(height: 400, color: Colors.blue[100]), VisibilityDetector( key: Key('tracked-widget'), onVisibilityChanged: (info) { setState(() { _visibleFraction = info.visibleFraction; if (info.visibleFraction == 0) { _status = 'Hidden'; } else if (info.visibleFraction == 1) { _status = 'Fully visible'; } else { _status = 'Partially visible'; } }); }, child: Container( height: 200, color: Colors.red[300], child: Center( child: Text( 'TRACKED WIDGET', style: TextStyle(fontSize: 24, color: Colors.white), ), ), ), ), Container(height: 600, color: Colors.green[100]), ], ), ), ], ); } } ``` -------------------------------- ### Implement SelfStoringCheckbox Source: https://context7.com/google/flutter.widgets/llms.txt Configures checkboxes with optional tri-state support for automatic state persistence. ```dart import 'package:flutter/material.dart'; import 'package:self_storing_input/self_storing_input.dart'; class NotificationSettings extends StatefulWidget { @override _NotificationSettingsState createState() => _NotificationSettingsState(); } class _NotificationSettingsState extends State { final OverlayController _overlayController = OverlayController(); final ApiSaver _saver = ApiSaver(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Notification Settings')), body: Padding( padding: EdgeInsets.all(16), child: Column( children: [ // Two-state checkbox (true/false only) SelfStoringCheckbox( 'email_notifications', saver: _saver, overlayController: _overlayController, tristate: false, title: Text('Email notifications'), ), SizedBox(height: 16), // Two-state checkbox SelfStoringCheckbox( 'push_notifications', saver: _saver, overlayController: _overlayController, tristate: false, title: Text('Push notifications'), ), SizedBox(height: 16), // Tri-state checkbox (true/false/null) SelfStoringCheckbox( 'marketing_emails', saver: _saver, overlayController: _overlayController, tristate: true, title: Text('Marketing emails (tri-state)'), ), ], ), ), ); } } ``` -------------------------------- ### Create Separated List with ScrollablePositionedList.separated Source: https://context7.com/google/flutter.widgets/llms.txt Use the 'separated' constructor of ScrollablePositionedList to create a list with separators between items. Requires importing 'package:scrollable_positioned_list/scrollable_positioned_list.dart'. ```dart import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class SeparatedListExample extends StatelessWidget { final ItemScrollController itemScrollController = ItemScrollController(); final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); @override Widget build(BuildContext context) { return ScrollablePositionedList.separated( itemCount: 100, itemScrollController: itemScrollController, itemPositionsListener: itemPositionsListener, itemBuilder: (context, index) => ListTile( title: Text('Item $index'), subtitle: Text('Description for item $index'), ), separatorBuilder: (context, index) => Divider( height: 1, thickness: 1, color: Colors.grey[300], ), ); } } ``` -------------------------------- ### Initialize TreeView with nested nodes Source: https://github.com/google/flutter.widgets/blob/master/packages/flutter_simple_treeview/README.md Construct a tree structure by nesting TreeNode objects within the nodes property of the TreeView widget. ```dart TreeView(nodes: [ TreeNode(content: Text("root1")), TreeNode( content: Text("root2"), children: [ TreeNode(content: Text("child21")), TreeNode(content: Text("child22")), TreeNode( content: Text("root23"), children: [ TreeNode(content: Text("child231")), ], ), ], ), ]), ``` -------------------------------- ### Implement GestureDetector for Overlays Source: https://github.com/google/flutter.widgets/blob/master/packages/self_storing_input/README.md Wrap the screen body in a GestureDetector to trigger the closing of editing overlays on tap. ```dart GestureDetector( onTap: () async { _controller.close(); }, child: Scaffold( body: ... ``` -------------------------------- ### Display a hierarchical tree with TreeView Source: https://context7.com/google/flutter.widgets/llms.txt Use the TreeView widget to render nested nodes with custom content, indentation, and icon sizes. ```dart import 'package:flutter/material.dart'; import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; class FileExplorerTree extends StatelessWidget { @override Widget build(BuildContext context) { return TreeView( indent: 40, iconSize: 24, nodes: [ TreeNode( content: Row( children: [ Icon(Icons.folder, color: Colors.amber), SizedBox(width: 8), Text('src'), ], ), children: [ TreeNode( content: Row( children: [ Icon(Icons.insert_drive_file, color: Colors.blue), SizedBox(width: 8), Text('main.dart'), ], ), ), TreeNode( content: Row( children: [ Icon(Icons.folder, color: Colors.amber), SizedBox(width: 8), Text('widgets'), ], ), children: [ TreeNode(content: Text('button.dart')), TreeNode(content: Text('card.dart')), ], ), ], ), TreeNode(content: Text('pubspec.yaml')), TreeNode(content: Text('README.md')), ], ); } } ``` -------------------------------- ### Jump to an item Source: https://github.com/google/flutter.widgets/blob/master/packages/scrollable_positioned_list/README.md Instantly jump to a specific index without animation. ```dart itemScrollController.jumpTo(index: 150); ``` -------------------------------- ### Monitor scroll offset changes Source: https://github.com/google/flutter.widgets/blob/master/packages/scrollable_positioned_list/README.md Listen to scroll offset changes via the experimental ScrollOffsetListener stream. ```dart scrollOffsetListener.changes.listen((event) => ...) ``` -------------------------------- ### Monitor visible items Source: https://github.com/google/flutter.widgets/blob/master/packages/scrollable_positioned_list/README.md Listen to changes in the list of visible items using the ItemPositionsListener. ```dart itemPositionsListener.itemPositions.addListener(() => ...); ``` -------------------------------- ### Implement SelfStoringRadioGroup Source: https://context7.com/google/flutter.widgets/llms.txt Configures radio button groups with optional unselectable behavior for automatic state persistence. ```dart import 'package:flutter/material.dart'; import 'package:self_storing_input/self_storing_input.dart'; class SubscriptionPlanSelector extends StatefulWidget { @override _SubscriptionPlanSelectorState createState() => _SubscriptionPlanSelectorState(); } class _SubscriptionPlanSelectorState extends State { final OverlayController _overlayController = OverlayController(); final ApiSaver _saver = ApiSaver(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Select Plan')), body: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Choose your plan:', style: Theme.of(context).textTheme.titleLarge), SizedBox(height: 16), SelfStoringRadioGroup( 'subscription_plan', saver: _saver, overlayController: _overlayController, defaultValue: 'basic', isUnselectable: false, items: { 'basic': 'Basic - Free', 'pro': 'Pro - \$9.99/month', 'enterprise': 'Enterprise - \$49.99/month', }, ), SizedBox(height: 32), Text('Billing cycle (optional):', style: Theme.of(context).textTheme.titleLarge), SizedBox(height: 16), // Unselectable radio group - can be cleared SelfStoringRadioGroup( 'billing_cycle', saver: _saver, overlayController: _overlayController, isUnselectable: true, items: { 'monthly': 'Monthly', 'yearly': 'Yearly (save 20%)', }, ), ], ), ), ); } } ``` -------------------------------- ### Lazy Loading Images with VisibilityDetector Source: https://context7.com/google/flutter.widgets/llms.txt Use VisibilityDetector to load images only when they become visible in a ListView. This optimizes performance by reducing the number of images loaded simultaneously. Images are marked as loaded and their appearance changes when they are at least partially visible. ```dart import 'package:flutter/material.dart'; import 'package:visibility_detector/visibility_detector.dart'; class LazyLoadingImages extends StatefulWidget { @override _LazyLoadingImagesState createState() => _LazyLoadingImagesState(); } class _LazyLoadingImagesState extends State { final Set _loadedImages = {}; @override Widget build(BuildContext context) { return ListView.builder( itemCount: 100, itemBuilder: (context, index) => VisibilityDetector( key: Key('image-$index'), onVisibilityChanged: (info) { if (info.visibleFraction > 0 && !_loadedImages.contains(index)) { setState(() { _loadedImages.add(index); }); debugPrint('Loading image $index (${(info.visibleFraction * 100).toStringAsFixed(1)}% visible)'); } }, child: Container( height: 200, margin: EdgeInsets.all(8), decoration: BoxDecoration( color: _loadedImages.contains(index) ? Colors.green[100] : Colors.grey[300], borderRadius: BorderRadius.circular(8), ), child: Center( child: _loadedImages.contains(index) ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image, size: 48, color: Colors.green), Text('Image $index loaded'), ], ) : Text('Image $index (not loaded)'), ), ), ), ); } } ``` -------------------------------- ### Track Scroll Offset with addOffsetChangedListener Source: https://context7.com/google/flutter.widgets/llms.txt Listen to scroll offset changes across all linked controllers using addOffsetChangedListener. Remember to remove the listener in the dispose method to prevent memory leaks. ```dart import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; class ScrollPositionTracker extends StatefulWidget { @override _ScrollPositionTrackerState createState() => _ScrollPositionTrackerState(); } class _ScrollPositionTrackerState extends State { late LinkedScrollControllerGroup _controllers; late ScrollController _listController; double _currentOffset = 0; @override void initState() { super.initState(); _controllers = LinkedScrollControllerGroup(); _listController = _controllers.addAndGet(); // Listen to offset changes _controllers.addOffsetChangedListener(_onOffsetChanged); } void _onOffsetChanged() { setState(() { _currentOffset = _controllers.offset; }); } @override void dispose() { _controllers.removeOffsetChangedListener(_onOffsetChanged); _listController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( children: [ Padding( padding: EdgeInsets.all(16), child: Text('Scroll Offset: ${_currentOffset.toStringAsFixed(2)}'), ), ElevatedButton( onPressed: () => _controllers.animateTo( 500, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ), child: Text('Animate to 500'), ), ElevatedButton( onPressed: () => _controllers.resetScroll(), child: Text('Reset to Top'), ), Expanded( child: ListView.builder( controller: _listController, itemCount: 100, itemBuilder: (context, index) => ListTile(title: Text('Item $index')), ), ), ], ); } } ``` -------------------------------- ### Synchronize Two ListViews with LinkedScrollControllerGroup Source: https://github.com/google/flutter.widgets/blob/master/packages/linked_scroll_controller/README.md Use `LinkedScrollControllerGroup` to create and manage scroll controllers for multiple widgets that need to scroll in unison. Ensure controllers are disposed of properly in `initState` and `dispose`. ```dart class LinkedScrollables extends StatefulWidget { @override _LinkedScrollablesState createState() => _LinkedScrollablesState(); } class _LinkedScrollablesState extends State { LinkedScrollControllerGroup _controllers; ScrollController _letters; ScrollController _numbers; @override void initState() { super.initState(); _controllers = LinkedScrollControllerGroup(); _letters = _controllers.addAndGet(); _numbers = _controllers.addAndGet(); } @override void dispose() { _letters.dispose(); _numbers.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Row( children: [ Expanded( child: ListView( controller: _letters, children: [ _Tile('Hello A'), _Tile('Hello B'), _Tile('Hello C'), _Tile('Hello D'), _Tile('Hello E'), ], ), ), Expanded( child: ListView( controller: _numbers, children: [ _Tile('Hello 1'), _Tile('Hello 2'), _Tile('Hello 3'), _Tile('Hello 4'), _Tile('Hello 5'), ], ), ), ], ), ); } } class _Tile extends StatelessWidget { final String caption; _Tile(this.caption); @override Widget build(_) => Container( margin: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0), height: 250.0, child: Center(child: Text(caption)), ); } ``` -------------------------------- ### Scroll to an item Source: https://github.com/google/flutter.widgets/blob/master/packages/scrollable_positioned_list/README.md Animate the list to a specific index using the ItemScrollController. ```dart itemScrollController.scrollTo( index: 150, duration: Duration(seconds: 2), curve: Curves.easeInOutCubic); ``` -------------------------------- ### Synchronize Tables with LinkedScrollControllerGroup Source: https://context7.com/google/flutter.widgets/llms.txt Use LinkedScrollControllerGroup to link the scroll controllers of a fixed header row and a scrollable body, making them scroll horizontally in sync. Ensure controllers are disposed properly. ```dart import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; class SynchronizedTablesExample extends StatefulWidget { @override _SynchronizedTablesExampleState createState() => _SynchronizedTablesExampleState(); } class _SynchronizedTablesExampleState extends State { late LinkedScrollControllerGroup _controllers; late ScrollController _headerController; late ScrollController _bodyController; @override void initState() { super.initState(); _controllers = LinkedScrollControllerGroup(); _headerController = _controllers.addAndGet(); _bodyController = _controllers.addAndGet(); } @override void dispose() { _headerController.dispose(); _bodyController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( children: [ // Fixed header row that scrolls horizontally Container( height: 50, color: Colors.grey[300], child: ListView.builder( controller: _headerController, scrollDirection: Axis.horizontal, itemCount: 20, itemBuilder: (context, index) => Container( width: 100, alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), child: Text('Column $index', style: TextStyle(fontWeight: FontWeight.bold)), ), ), ), // Body rows that scroll horizontally in sync with header Expanded( child: ListView.builder( controller: _bodyController, scrollDirection: Axis.horizontal, itemCount: 20, itemBuilder: (context, columnIndex) => Container( width: 100, child: ListView.builder( itemCount: 50, itemBuilder: (context, rowIndex) => Container( height: 40, alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(color: Colors.grey[200]!), ), child: Text('Cell $columnIndex-$rowIndex'), ), ), ), ), ), ], ); } } ``` -------------------------------- ### Animate scroll offset Source: https://github.com/google/flutter.widgets/blob/master/packages/scrollable_positioned_list/README.md Perform a relative scroll animation by a specific pixel offset. ```dart scrollOffsetController.animateScroll(offset: 100, duration: Duration(seconds: 1)); ``` -------------------------------- ### Waiting for Callbacks in Widget Tests Source: https://github.com/google/flutter.widgets/blob/master/packages/visibility_detector/README.md Alternatively, to avoid setting the update interval to zero in widget tests, you can wait for the callbacks to fire using tester.pump with the controller's update interval. ```dart await tester.pump(VisibilityDetectorController.instance.updateInterval); ``` -------------------------------- ### Setting Update Interval for Widget Tests Source: https://github.com/google/flutter.widgets/blob/master/packages/visibility_detector/README.md In widget tests, set VisibilityDetectorController.instance.updateInterval to Duration.zero to ensure immediate visibility change reporting and avoid timer-related assertions during widget tree teardown. ```dart VisibilityDetectorController.instance.updateInterval = Duration.zero; ``` -------------------------------- ### Avoiding Timer Assertion in Widget Tests Source: https://github.com/google/flutter.widgets/blob/master/packages/visibility_detector/README.md To prevent the 'Timer is still pending...' assertion when tearing down the widget tree in tests without modifying the update interval, explicitly destroy the widget tree before the test completes. ```dart await tester.pumpWidget(Placeholder()); ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.