5

This post describes a very similar problem, but the answer there doesn't solve all problems:

I have a potentially long List, where the user can add new items (on at a time). After/On add, the list should scroll to its end.

(Btw no, reverse: true is not an option)

After reading the other post, I understood using SchedulerBinding.instance.addPostFrameCallback((_) => scrollToEnd()); should to the trick b/c the new lists maxScrollExtent will be correct.

But it doesn't work reliably: When already scrolled to the end of the list or near the end everything's ok. But when the list is scrolled to its start (or some way from the end) when adding a new item, the list gets scrolled, but the scrollposition is off by exactly one item - the newest one.

I think it might have something to do with the ListView.builder not keeping all children alive - but how to solve it?

Oh and bonus question: just discovered another very strange behaviour: after adding two items the last one is a little bit out of view but the list isn't scrollable - which is strange. But even stranger is that on the next add-item-click the list scrolls this tiny bit - but without ever creating the new item!?

Here a complete example:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() {
  runApp(MyList());
}

class MyList extends StatefulWidget {
  MyList({Key key}) : super(key: key);

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

var items = List<String>.generate(8, (i) => "Item $i");

class _MyListState extends State<MyList> {
  static ScrollController _scrollController = ScrollController();

  void add() {
    setState(() {
      items.add("new Item ${items.length}");
      print(items.length);
    });
    SchedulerBinding.instance.addPostFrameCallback((_) => scrollToEnd());
  }

  void scrollToEnd() {
    _scrollController.animateTo(_scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 350), curve: Curves.easeOut);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "List",
      home: Scaffold(
          appBar: AppBar(
            title: Text("List"),
          ),
          body: ListView.builder(
            controller: _scrollController,
            itemCount: items.length,
            shrinkWrap: true,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('${items[index]}'),
              );
            },
          ),
          bottomSheet: Container(
              decoration: BoxDecoration(
                  border:
                      Border(top: BorderSide(color: Colors.black, width: 1))),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  FloatingActionButton(
                    onPressed: () {
                      add();
                    },
                    child: Icon(Icons.add),
                  )
                ],
              ))),
    );
  }
}
user3249027
  • 545
  • 5
  • 17

2 Answers2

8

I combined scroll to maxScrollExtent with Scrollable.ensureVisible and each of them fixed the flaws of the other.

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

class MyList extends StatefulWidget {
  MyList({Key key}) : super(key: key);

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

class _MyListState extends State<MyList> {
  final ScrollController _scrollController = ScrollController();
  final lastKey = GlobalKey();
  List<String> items;

  @override
  void initState() {
    super.initState();
    items = List<String>.generate(8, (i) => "Item $i");
  }

  void add() {
    setState(() {
      items.add("new Item ${items.length}");
    });
    SchedulerBinding.instance.addPostFrameCallback((_) => scrollToEnd());
  }

  void scrollToEnd() async {
    await _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 350),
        curve: Curves.easeOut);
    Scrollable.ensureVisible(lastKey.currentContext);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: "List",
        home: Scaffold(
          body: ListView.builder(
            controller: _scrollController,
            itemCount: items.length,
            shrinkWrap: true,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('${items[index]}'),
                key: index == items.length - 1 ? lastKey : null,
              );
            },
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              add();
            },
            child: Icon(Icons.add),
          ),
        ));
  }
}

Scrollable.ensureVisible itself cannot provide visibility if the item has not yet been created, but copes with them when item is very close.

Spatz
  • 18,640
  • 7
  • 62
  • 66
  • awesome, thank you! That works. But it still feels like a workaround. Do you have any explanation why this double-feature is necessary? Or any idea about that strange missing list item? – user3249027 Aug 26 '19 at 09:39
  • 3
    Yes, it's workaround. I believe that the bug with maxScrollExtent calculation is caused by a conflict between dynamically created elements and a new element. – Spatz Aug 26 '19 at 09:56
  • thanks. I'll see to find the time to file a bug report @flutter. As long no one brings up a (even) better answer / solution, this one is the accepted answer. Thanks buddy! – user3249027 Aug 26 '19 at 15:10
2

Specifying a too large scrollPosition works without errors, the ScrollController then automatically scrolls to the final maximum value. I define a _scrollController and execute the following command:

_scrollController.animateTo(
    _scrollController.position.maxScrollExtent + 200,
    duration: const Duration(milliseconds: 350),
    curve: Curves.easeOut);

or

_scrollController.jumpTo(_scrollController.position.maxScrollExtent + 200);
oibees
  • 129
  • 1
  • 3
  • hey, sorry, for the late reply.. Thats a good suggestion, but does it work with Overscroll - or BouncePhysics too? I fear it's only got for ClampingPhysics? – user3249027 Jan 09 '22 at 08:06