google / flutter.widgets

https://pub.dev/packages/flutter_widgets
BSD 3-Clause "New" or "Revised" License
1.36k stars 469 forks source link

[linked_scroll_controller] How to add unique keys to scrollables when linking dynamically? #68

Open yasinarik opened 4 years ago

yasinarik commented 4 years ago

If you add controllers dynamically, the corresponding scrollables must be given unique keys to avoid the scroll offset going out of sync.

LinkedScrollControllerGroup controllers;
List scrollCTRLList;  

createAndLinkControllers(count) {
    for (var i = 0; i < count; i++) {
      var scrollControllerInstance = ScrollController();
      scrollControllerInstance = controllers.addAndGet();
      scrollCTRLList.add(scrollControllerInstance);
    }
  }

I have been building a custom data table widget for FlutterWeb use in particular. It can be scrolled in both directions. There is a single ListView.builder (for horizontal scrolling) and it holds dynamically added ListView.builders (for vertical scrolling as columns).

I've just figured out that the vertical scrollers go out of sync, unfortunately. It happens when some vertical ListView.builders are out of the visible area.

As I said, I can scroll horizontally. For example, 0-1-2-3-4 columns are visible. I scroll down to an arbitrary point such as somewhere near the 250th row. Then I scroll to right to see the 3-4-5-6 columns. The columns 5-6 were invisible until I scroll. At that moment they are out of sync.

If I then scroll up or down they become in sync immediately.

How can I solve that?

yasinarik commented 4 years ago

Currently, I have just found a hacky solution myself. I tried changing the cacheExtend property of the Horizontal ListView.builder().

This solved the issue. However, putting a cacheExtend is basically telling the horizontal ListView.builder not to dispose/kill the invisible columns. Indeed, .builder() is used for render on-demand . So I have basically turned ListView.builder() into a basic ListView.

The weird thing is that when I tried doing it without a builder, with the basic ListView to prevent killing the invisible columns, the out of sync issue still occurred.

jamesderlin commented 4 years ago

If you just want to know how to add unique keys, can't you use, say, ValueKey<String>s where the strings embeds row/column information? (I'm not familiar with using package:linked_scroll_controller, so I don't actually know.)

rbluethl commented 4 years ago

@yasinarik I have the same problem. The visible rows are perfectly kept in sync, however as I scroll down, the elements are becoming out of sync UNTIL another manual scroll occurs. That's pretty disappointing. :( Increasing the cacheExtent seems to work, although I think that it defeats the purpose of using ListView.builder (just as you said). Is there another solution to this problem?

yasinarik commented 4 years ago

@rbluethl Sorry to say that but I couldn’t find a solution for this so I just skipped it. I don’t render rows on demand. This is a loss. I don’t know if it’s an acceptable loss however.

At least there are a limited amount of rows in a data table I would ever need in my application.

I’m using the linker because I built (am building) a Google Spreadsheet like tool where you can resize and reorder columns individually. I’m just making sure that the extent cache property is set accordingly to the total scrollable column widths. Or you can just write an arbitrarily big number like 20000.

jamesderlin commented 4 years ago

Sample code that can reproduce the problem probably would help a lot.

rbluethl commented 4 years ago

It would be great if there were an example on how to use linked_scroll_controller with dynamically added ScrollControllers. It's pretty hard to figure out how to do it. Controllers that are added dynamically with ListView.builer are getting totally out of sync. Somehow these controllers are not correctly synchronized.

pedromassango commented 4 years ago

I had this issue and the following error message on Fluter web while I was trying to resize the flutter web app: _LinkedScrollPosition cannot change controllers once created.. To solve this you need to make sure your dinamic ListView does not try to update the LinkedScrollControllerGroup during the build phase (make sure to create your dynamic controllers only once and re-use it). basically don't call _linkedScrollControllerGroup.addAndGet() inside the build method.

Basically what the docs says is: for each scrollable ( your ListView) you need to add a unique key to prevent the sync issue. Here is how you add a unique jey to a scrollable:

Expanded(
            child: ListView.separated(
              key: ObjectKey(data), // This is the custom key
              controller: controller,
              itemCount: data.length,
              itemBuilder: (context, index) {
                final item = data[index];
                return _MessageListItem(item: item);
              },
              separatorBuilder: (context, index) {
                return _CustomDivider();
              },
            ),
          )

Note that each ListView's key must be created with a different data!

philipgiuliani commented 4 years ago

Hi! This is not working for me... The rows still go out of sync when I scroll.

class _HomePageState extends State<HomePage> {
  final controllerGroup = LinkedScrollControllerGroup();
  List<ScrollController> scrollControllers;

  int channelCount = 80;

  @override
  void initState() {
    scrollControllers = List<ScrollController>(channelCount);
    for (int i = 0; i < channelCount; i++) {
      scrollControllers[i] = controllerGroup.addAndGet();
    }
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: channelCount,
      itemExtent: 38,
      itemBuilder: (BuildContext context, int i) {
        return ListView.builder(
          key: Key('Test-$i'),
          controller: scrollControllers[i],
          itemCount: 100,
          scrollDirection: Axis.horizontal,
          itemBuilder: (BuildContext context, int i) {
            return Container(
              margin: EdgeInsets.all(4),
              height: 30,
              width: 100,
              color: Colors.red,
            );
          },
        );
      },
    );
  }
}

Screenshot 2020-07-20 at 15 26 56

rbluethl commented 3 years ago

Any news? We're still facing this issue.

deadsoul44 commented 3 years ago

Any update?

deadsoul44 commented 3 years ago

I had the same problem when creating something like custom datatable which has horizontally scrollable table rows. When I sorted the table, table rows and header were going out of sync. I solved the problem by creating the keys also in init method. I am not sure if this is related to the OP though.

deadsoul44 commented 3 years ago

Ok, I have the same problem when number of rows is higher than 18. Interestingly, it still doesn't work even when I set cacheExtend: 900000.0.

deadsoul44 commented 3 years ago

Ok, I solved it by adding a controller to the vertical listview bilder.

    _verticalListController.addListener(() {
      _linkedScrollControllerGroup.jumpTo(_linkedScrollControllerGroup.offset);
    });

The package could have done this itself by jumping newly attached scrollers to the correct offset.

ngdangduy13 commented 3 years ago

I had this issue and the following error message on Fluter web while I was trying to resize the flutter web app: _LinkedScrollPosition cannot change controllers once created.. This solve this you need to make sure your dinamic ListView does not try to update the LinkedScrollControllerGroup during the build phase (make sure to create your dynamic controllers only once and re-use it). basically don't call _linkedScrollControllerGroup.addAndGet() inside the build method.

Basically what the docs says is: for each scrollable ( your ListView) you need to add a unique key to prevent the sync issue. Here is how you add a unique jey to a scrollable:

Expanded(
            child: ListView.separated(
              key: ObjectKey(data), // This is the custom key
              controller: controller,
              itemCount: data.length,
              itemBuilder: (context, index) {
                final item = data[index];
                return _MessageListItem(item: item);
              },
              separatorBuilder: (context, index) {
                return _CustomDivider();
              },
            ),
          )

Note that each ListView's key must be created with a different data!

This worked for me

AlexLomm commented 1 year ago

Ok, I solved it by adding a controller to the vertical listview bilder.

    _verticalListController.addListener(() {
      _linkedScrollControllerGroup.jumpTo(_linkedScrollControllerGroup.offset);
    });

The package could have done this itself by jumping newly attached scrollers to the correct offset.

This must be the most appropriate solution!

One caveat, though. In some cases _linkedScrollControllerGroup.offset was instantly updated to the wrong value, so the feature was still broken for me.

I've solved this issue by not directly using the _linkedScrollControllerGroup.offset but rather by setting a "temp" variable to the horizontal offset and using that value to jump to subsequently. Example:

  // ...
  double _horizontalScrollOffset = 0;

  @override
  void initState() {
    super.initState();

    // setting the horizontal offset to the "temp" variable
    _horizontalScrollControllersGroup = LinkedScrollControllerGroup()..addOffsetChangedListener(() {
      _horizontalScrollOffset = _horizontalScrollControllersGroup.offset;
    });

    // jumping to that offset when scrolling vertically
    _verticalScrollController = ScrollController()
      ..addListener(() {
        _horizontalScrollControllersGroup.jumpTo(_horizontalScrollOffset);
      });

    // ...
  }
  // ...