flutterflutter-listviewflutter-packages

How to combine ScrollablePositionedList with Navigator push/pop without states


My app combines ListView and Navigator (push and pop) to browse (sub)sections of a document, starting from a user-supplied keyword or a TOC entry (with a TextButton for each section in the document). The same stateless widgets are used by all Navigator routes.

To obtain initial scrolling to a particular item (typically the line of the keyword) on a page, I replaced ListView by the ScrollablePositionedList package (SPL), which provides initialScrollIndex. This works fine for the first route (Scaffold page), but on Navigator.push, the app freezes, without throwing an exception.

A minimal test program using ListView and Navigator works fine, but freezes when ListView is replaced by SPL. I assume this could be related to the ItemScrollController used by SPL.

Thanks for your advice.

import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

List<String> data = [for(var i=0; i<100; i++) '-- Item $i --'];

void main() => runApp(MaterialApp(title: 'Test', home: HomePage()));

final ItemScrollController itemScrollController = ItemScrollController();

class HomePage extends StatelessWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context) {
    var listAll = [for(var i=0; i<data.length; i++) i];  // list of indexes in data[]
    var listPartim = [for(var i=20; i<40; i++) i]; // list of indexes in data[]
    int hot = 0;  // hotspot item (to highligthed in list) 
    return Scaffold(
      appBar: AppBar(
        title: Row(children: [
          TextButton(onPressed: () => itemScrollController.scrollTo(index: 20, 
              duration: Duration(seconds: 1), curve: Curves.easeInOutCubic), 
            child: Text('Scroll to item 20 (ONLY when using ScrollablePositionedList)')),
          TextButton(onPressed: () { Navigator.push(context,
              MaterialPageRoute(builder: (context) => SecondPage(list: listPartim, hot: hot, title: '')));},
            child: Text('Navigate to page 2')),
          ],),),
      body: MyScroll(listAll, 25),  // Item 25 is hotspot
      );
  }
}

class SecondPage extends StatelessWidget {
  final List<int> list; // list of entryId
  final int hot;        // entryId of hotspot
  final String title;
  const SecondPage({super.key, required this.list, required this.hot, required this.title});
  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Text(title),),
      body: MyScroll(list, hot), 
    );
}

class MyScroll extends StatelessWidget {
  final List<int> list; final int hotItem;
  const MyScroll(this.list, this.hotItem, {super.key});
  @override
  Widget build(BuildContext context) => ScrollablePositionedList.separated( 
      itemCount: list.length,
      initialScrollIndex: hotItem,
      itemBuilder: (context, index) => MyItem(index, hotItem),
      itemScrollController: itemScrollController,
      separatorBuilder: (context, int index) => const Divider(height: 12, thickness: 1),
    );
}

class MyItem extends StatelessWidget {
  final int i, hot;
  const MyItem(this.i, this.hot, {super.key});
  @override
  Widget build(BuildContext context) => Center(heightFactor: 3, child: 
    Container(height: 20.0, color: (i == hot) ? Colors.green :  Colors.white, child: Text(data[i])));
}

Solution

  • Make the itemScrollController a parameter to MyScroll

    class MyScroll extends StatelessWidget {
      final List<int> list; final int hotItem;
      final ItemScrollController itemScrollController;
      const MyScroll(this.list, this.hotItem, this.itemScrollController, {super.key});
      @override
      Widget build(BuildContext context) => ScrollablePositionedList.separated(
        itemCount: list.length,
        initialScrollIndex: hotItem,
        itemBuilder: (context, index) => MyItem(index, hotItem),
        itemScrollController: itemScrollController,
        separatorBuilder: (context, int index) => const Divider(height: 12, thickness: 1),
      );
    }
    
    

    Then define itemScrollController for each page right inside the class, pass that to MyScroll

    
    class HomePage extends StatelessWidget {
       HomePage({super.key});
    
    
      final ItemScrollController itemScrollController = ItemScrollController();
    
      @override
      Widget build(BuildContext context) {
        var listAll = [for(var i=0; i<data.length; i++) i];  // list of indexes in data[]
        var listPartim = [for(var i=20; i<40; i++) i]; // list of indexes in data[]
        int hot = 0;  // hotspot item (to highligthed in list)
        return Scaffold(
          appBar: AppBar(
            title: Row(children: [
              Expanded(
                child: TextButton(onPressed: () => itemScrollController.scrollTo(index: 20,
                    duration: Duration(seconds: 1), curve: Curves.easeInOutCubic),
                    child: Text('Scroll to item 20 (ONLY when using ScrollablePositionedList)')),
              ),
              Expanded(
                child: TextButton(onPressed: () { Navigator.push(context,
                    MaterialPageRoute(builder: (context) => SecondPage(list: listPartim, hot: hot, title: '')));},
                    child: Text('Navigate to page 2')),
              ),
            ],),),
          body: MyScroll(listAll, 25, itemScrollController),  // Item 25 is hotspot
        );
      }
    }
    
    class SecondPage extends StatelessWidget {
      final List<int> list; // list of entryId
      final int hot;        // entryId of hotspot
      final String title;
       SecondPage({super.key, required this.list, required this.hot, required this.title});
    
      final ItemScrollController itemScrollController = ItemScrollController();
      @override
      Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(title),),
        body: MyScroll(list, hot, itemScrollController),
      );
    }