11

i have a screen that build using MaterialApp, DefaultTabController, Scaffold and TabBarView.

in this screen, i have body content that retreive a list of element from sqllite using StreamBuilder. i get exact 100 elements ("finite list") to be shown using ListView.

my question, using ListView.builder, How we can jump to certain index when this screen opened ?

my main screen:

...
ScrollController controller = ScrollController();

 @override
  Widget build(BuildContext context) {

    return MaterialApp(
      debugShowCheckedModeBanner : false,
      home: DefaultTabController(
        length: 3,
        child: Scaffold(
            appBar: AppBar(
              backgroundColor: Pigment.fromString(UIData.primaryColor),
              elevation: 0,
              centerTitle: true,
              title: Text(translations.text("quran").toUpperCase()),
              bottom: TabBar(
                tabs: [
                    Text("Tab1"),
                    Text("Tab2"),
                    Text("Tab3")
                ],
              ),
              leading: Row(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[
                  Expanded(
                      child: InkWell(
                        child: SizedBox(child: Image.asset("assets/images/home.png"), height: 10, width: 1,),
                        onTap: () => Navigator.of(context).pop(),
                      )
                  ),
                ],
              ),
            ),

            floatingActionButton: FloatingActionButton(
              onPressed: _scrollToIndex,
              tooltip: 'Testing Index Jump',
              child: Text("GO"),
            ),

            body:
            TabBarView(
              children: [
                Stack(
                  children: <Widget>[
                    MyDraggableScrollBar.create(
                        scrollController: controller,
                        context: context,
                        heightScrollThumb: 25,
                        child: ListView(
                          controller: controller,
                          children: <Widget>[
                            Padding(
                                padding: EdgeInsets.fromLTRB(30, 15, 30, 8),
                                child: Container(
                                    alignment: Alignment.center,
                                    height: 30,
                                    child: ClipRRect(
                                      borderRadius: BorderRadius.circular(8),
                                      child: TextField(
                                        style: TextStyle(color: Colors.green),
                                        decoration: new InputDecoration(
                                            contentPadding: EdgeInsets.all(5),
                                            border: InputBorder.none,
                                            filled: true,
                                            hintStyle: new TextStyle(color: Colors.green, fontSize: 14),
                                            prefixIcon: Icon(FontAwesomeIcons.search,color: Colors.green,size: 17,),
                                            hintText: translations.text("search-quran"),
                                            fillColor: Colors.grey[300],
                                            prefixStyle: TextStyle(color: Colors.green)
                                        ),
                                        onChanged: (val) => quranBloc.searchSurah(val),
                                      ),
                                    )
                                )
                            ),

                            //surah list
                            streamBuilderQuranSurah(context)

                          ],
                        )
                    ) // MyDraggableScrollBar

                  ],
                ),
                Icon(Icons.directions_transit),
                Icon(Icons.directions_bike),
              ],
            )
        )));
  }

  Widget streamBuilderQuranSurah(BuildContext ctx){
    return StreamBuilder(
      stream: quranBloc.chapterStream ,
      builder: (BuildContext context, AsyncSnapshot<ChaptersModel> snapshot){
        if(snapshot.hasData){

          return ListView.builder(
            controller: controller,
            shrinkWrap: true,
            physics: NeverScrollableScrollPhysics(),
            itemCount:(snapshot.data.chapters?.length ?? 0),
            itemBuilder: (BuildContext context, int index) {
              var chapter =
              snapshot.data.chapters?.elementAt(index);
              return chapterDataCell(chapter);
            },
          );
        }
        else{

          return SurahItemShimmer();
        }
      },
    );
  }
...

class MyDraggableScrollBar.dart :

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

class MyDraggableScrollBar {
  static Widget create({
    @required BuildContext context,
    @required ScrollController scrollController,
    @required double heightScrollThumb,
    @required Widget child,
  }) {
    return DraggableScrollbar(

      alwaysVisibleScrollThumb: true,
      scrollbarTimeToFade: Duration(seconds: 3),
      controller: scrollController,
      heightScrollThumb: heightScrollThumb,
      backgroundColor: Colors.green,
      scrollThumbBuilder: (
        Color backgroundColor,
        Animation<double> thumbAnimation,
        Animation<double> labelAnimation,
        double height, {
        Text labelText,
        BoxConstraints labelConstraints,
      }) {
        return InkWell(
          onTap: () {},
          child: Container(
            height: height,
            width: 7,
            color: backgroundColor,
          ),
        );
      },
      child: child,
    );
  }
}

i have tried find other solutions but seems not working, for example indexed_list_view that only support infinite list

and it seems flutter still not have feature for this, see this issue

Any Idea ?

questionasker
  • 2,536
  • 12
  • 55
  • 119

7 Answers7

5

You can use https://pub.dev/packages/scrollable_positioned_list. You can pass the initial index to the widget.

ScrollablePositionedList.builder(
 initialScrollIndex: 12, //you can pass the desired index here//
 itemCount: 500,
 itemBuilder: (context, index) => Text('Item $index'),
 itemScrollController: itemScrollController,
 itemPositionsListener: itemPositionsListener,
);
yuktatn
  • 53
  • 1
  • 3
3

General Solution:

To store anything which can be represented as a number/string/list of strings, Flutter provides a powerful easy-to-use plugin which stores the values needed to be stored along with a key. So the next time you need you'll need to retrieve or even update that value all that you'll need is that key.

To get started, add the shared_preferences plugin to the pubspec.yaml file,

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: "<newest version>"

Run flutter pub get from the terminal or if your using IntelliJ just click on Packages get(You'll find it somewhere around the top-right corner of your screen while viewing the pubspec.yaml file)

Once the above command is successfully executed, import the below file in your main.dart or concerned file.

  import 'package:shared_preferences/shared_preferences.dart';

Now just attach a ScrollController to your ListView.builder() widget and make sure that the final/last offset is stored along with a specific key using shared_preferences whenever the user leaves the app in any way and is set when the initState of your concerned widget is called.

In order to know to detect changes in the state of our app and to act with accordance to it, we'll be inheriting WidgetsBindingObserver to our class.

Steps to follow:

  1. Extend the WidgetsBindingObserver class along with the State class of your StatefulWidget.

  2. Define a async function resumeController() as a function member of the above class.

  Future<void> resumeController() async{
    _sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
      if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
      else _sharedPreferences.setDouble("scroll-offset-0", 0);
      setState((){});
      return _sharedPreferences;
    });
  1. Declare two variables one to store and pass the scrollcontroller and the other to store and use the instance of SharedPreferences.
  ScrollController _scrollController;
  SharedPreferences _sharedPreferences;
  1. Call resumeController() and pass your class to the addObserver method of the instance object in WidgetsBinding class.
  resumeController();
  WidgetsBinding.instance.addObserver(this);
  1. Simply paste this code in the class definition (outside other member functions)
 @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
       _sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
    super.didChangeAppLifecycleState(state);
  }
  1. Pass the ScrollController() to the concerned Scrollable.

Working Example:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver{

  //[...]
  ScrollController _scrollController;
  SharedPreferences _sharedPreferences;

  Future<void> resumeController() async{
    _sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
      if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
      else _sharedPreferences.setDouble("scroll-offset-0", 0);
      setState((){});
      return _sharedPreferences;
    });

  }

  @override
  void initState() {
    resumeController();
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
       _sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
    super.didChangeAppLifecycleState(state);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text("Smart Scroll View"),
        ),
        body: ListView.builder(
            itemCount: 50,
            controller: _scrollController,
            itemBuilder: (c,i)=>
                Padding(
                  padding: EdgeInsets.symmetric(horizontal: 24,vertical: 16),
                  child: Text((i+1).toString()),
                ),
        ),
      ),
    );
  }
}
  • Do you think `AppLifecycleState.paused`, `AppLifecycleState.inactive`, `AppLifecycleState.suspending` cover all cases and the scroll offset will always be saved, no matter how the app is "closed"? – TechAurelian May 10 '20 at 08:15
  • Also, `suspending` is no (longer a) value of [AppLifecycleState](https://api.flutter.dev/flutter/dart-ui/AppLifecycleState-class.html) – TechAurelian May 10 '20 at 08:18
1

Solution without knowing the size of your widgets

the Solution I found without knowing the size of your widget is displaying a reverse 'sublist' from the index to the end, then scroll to the top of your 'sublist' and reset the entire list. As it is a reverse list the item will be add at the top of the list and you will stay at your position (the index).

the problem is that you can't use a listView.builder because you will need to change the size of the list

example

class _ListViewIndexState extends State<ListViewIndex> {
  ScrollController _scrollController;
  List<Widget> _displayedList;
  @override
  void initState() {
    super.initState();

    _scrollController = ScrollController();

    _displayedList = widget.items.sublist(0, widget.items.length - widget.index);

    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance.addPostFrameCallback((_) {
//here the sublist is already build
        completeList();
      });
    }
  }

  completeList() {
//to go to the last item(in first position) 
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
//reset the list to the full list
    setState(() {
      _displayedList = widget.items;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        ListView(
          controller: _scrollController,
          reverse: true,
          children: _displayedList,
        ),
      ]
    );
  }
}
Community
  • 1
  • 1
Shinbly
  • 47
  • 6
1

The https://pub.dev/packages/indexed_list_view package could maybe help you out for this. Use something like this:

IndexedListView.builder(
    controller: indexScrollController, 
    itemBuilder: itemBuilder
);


indexScrollController.jumpToIndex(10000);
Benjamin Corben
  • 277
  • 3
  • 10
1

I'll present another approach, which supports list lazy loading unlike @Shinbly 's method, and also support tiles in list to resize without recalculating the correct offset of the ListView nor saving any persistent information like "@Nephew of Stackoverflow" does.

The essential key to this approach is to utilize CustomScrollView, the CustomScrollView.center property.

Here's an example based on the example code from Flutter document (widgets.CustomScrollView.2):

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  List<int> top = [];
  List<int> bottom = [0];
  List<int> test = List.generate(10, (i) => -5 + i);
  bool positionSwitcher = true;

  @override
  Widget build(BuildContext context) {
    positionSwitcher = !positionSwitcher;
    final jumpIndex = positionSwitcher ? 1 : 9;
    Key centerKey = ValueKey('bottom-sliver-list');
    return Scaffold(
      appBar: AppBar(
        title: const Text('Press Jump!! to jump between'),
        leading: IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            setState(() {
              top.add(-top.length - 1);
              bottom.add(bottom.length);
            });
          },
        ),
      ),
      body: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              RaisedButton(
                child: Text('Jump!!'),
                onPressed: () => setState(() {}),
              ),
              Text(positionSwitcher ? 'At top' : 'At bottom'),
            ],
          ),
          Expanded(
            child: CustomScrollView(
              center: centerKey,
              slivers: <Widget>[
                SliverList(
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int i) {
                      final index = jumpIndex - 1 - i;
                      return Container(
                        alignment: Alignment.center,
                        color: Colors.blue[200 + test[index] % 4 * 100],
                        height: 100 + test[index] % 4 * 20.0,
                        child: Text('Item: ${test[index]}'),
                      );
                    },
                    childCount: jumpIndex,
                  ),
                ),
                SliverList(
                  key: centerKey,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int i) {
                      final index = i + jumpIndex;
                      return Container(
                        alignment: Alignment.center,
                        color: i == 0
                            ? Colors.red
                            : Colors.blue[200 + test[index] % 4 * 100],
                        height: 100 + test[index] % 4 * 20.0,
                        child: Text('Item: ${test[index]}'),
                      );
                    },
                    childCount: test.length - jumpIndex,
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

Explanation:

  1. We use single list as data source for both SliverList
  2. During each rebuild, we use center key to reposition the second SliverList inside ViewPort
  3. Carefully manage the conversion from SliverList index to data source list index
  4. Notice how the scroll view build the first SliverList by passing an index starting from bottom of this SliverList (i.e. index 0 suggests last item in the first list sliver)
  5. Give the CustomeScrollView a proper key to decide whether to "re-position" or not
wangkaibule
  • 808
  • 1
  • 9
  • 20
0

Working Example:

import 'dart:math';

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

class ScrollToIndexDemo extends StatefulWidget {
  const ScrollToIndexDemo({Key? key}) : super(key: key);

  @override
  _ScrollToIndexDemoState createState() => _ScrollToIndexDemoState();
}

class _ScrollToIndexDemoState extends State<ScrollToIndexDemo> {
  late AutoScrollController controller = AutoScrollController();
  var rng = Random();
  ValueNotifier<int> scrollIndex = ValueNotifier(0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: ValueListenableBuilder(
          valueListenable: scrollIndex,
          builder: (context, index, child) {
            return Text('Scroll Demo - $index');
          },
        ),
      ),
      body: ListView.builder(
        itemCount: 100,
        controller: controller,
        itemBuilder: (context, index) {
          return Padding(
            padding: EdgeInsets.all(8),
            child: AutoScrollTag(
              key: ValueKey(index),
              controller: controller,
              index: index,
              highlightColor: Colors.black.withOpacity(0.1),
              child: Container(
                padding: EdgeInsets.all(10),
                alignment: Alignment.center,
                color: Colors.grey[300],
                height: 100,
                child: Text(
                  'index: $index',
                  style: TextStyle(color: Colors.black),
                ),
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          scrollIndex.value = rng.nextInt(100);
          await controller.scrollToIndex(scrollIndex.value, preferPosition: AutoScrollPosition.begin);
        },
        tooltip: 'Increment',
        child: Center(
          child: Text(
            'Next',
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }
}
Raj Donga
  • 11
  • 2
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 23 '21 at 08:40
0

You can use the flutter_scrollview_observer lib to implement your desired functionality without invasivity

Create and use instance of ScrollController normally.

ScrollController scrollController = ScrollController();

ListView _buildListView() {
  return ListView.builder(
    controller: scrollController,
    ...
  );
}

Create an instance of ListObserverController pass it to ListViewObserver

ListObserverController observerController = ListObserverController(controller: scrollController);

ListViewObserver(
  controller: observerController,
  child: _buildListView(),
  ...
)

Now you can scroll to the specified index position

// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)

// Jump to the specified index position with animation.
observerController.animateTo(
  index: 1,
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);
LinXunFeng
  • 41
  • 3