flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.4k stars 27.29k forks source link

Improper Rendering of Items with Large Data When Vertical Scrolling. #151297

Open abineshPalanisamy opened 3 months ago

abineshPalanisamy commented 3 months ago

Steps to reproduce

  1. Run the attached sample.
  2. Scroll the list view vertically.
  3. Once the list reaches approximately 500,000 records, items become improperly aligned.

Expected results

Items should render properly even with a large record count. The heights of the items should be maintained correctly.

Actual results

Code sample

Code sample ```dart // ignore_for_file: must_be_immutable import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:collection/collection.dart'; void main() { runApp(MaterialApp( home: Scaffold( appBar: AppBar( title: Text('Custom List Sample'), ), body: CustomList(), ), )); } class CustomList extends StatelessWidget { CustomList({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return ListContainerWidget(constraints); }); } } class ListContainerWidget extends StatefulWidget { const ListContainerWidget(this.constraints, {super.key}); final BoxConstraints constraints; @override State createState() => _ListContainerWidgetState(); } class _ListContainerWidgetState extends State { double _width = 0.0; double _height = 0.0; ScrollController? _verticalController; ItemsContainer items = ItemsContainer.instance; late ItemsBuilder data; @override void initState() { super.initState(); _verticalController = ScrollController(); _height = widget.constraints.maxHeight + 50; _width = widget.constraints.maxWidth; _verticalController!.addListener(_verticalListener); data = items.fetchItemsDetails(); } @override void dispose() { _verticalController!.dispose(); super.dispose(); } void _verticalListener() { if (mounted) { setState(() { ItemsContainer.instance.verticalOffset = _verticalController!.offset; ItemsContainer.instance.fetchItemsDetails(); _height = _height + 1000000; }); } } @override Widget build(BuildContext context) { return SingleChildScrollView( controller: _verticalController, scrollDirection: Axis.vertical, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: DisplayArea( Size(_width, _height), data: data, )), ); } } class DisplayArea extends StatefulWidget { const DisplayArea(this.containerSize, {required this.data, super.key}); final Size containerSize; final ItemsBuilder data; @override State createState() => _DisplayAreaState(); } class _DisplayAreaState extends State { @override Widget build(BuildContext context) { final List children = []; children.addAll(widget.data.items .map((ItemsDetails item) => item.element) .toList()); return ContainerWidget( key: widget.key, containerSize: widget.containerSize, children: children, ); } } class ContainerWidget extends StatefulWidget { const ContainerWidget( {required this.containerSize, required this.children, super.key}); final Size containerSize; final List children; @override State createState() => _ContainerWidgetState(); } class _ContainerWidgetState extends State { @override Widget build(BuildContext context) { return ContainerRenderObjectWidget(widget.containerSize, widget.children); } } class ContainerRenderObjectWidget extends MultiChildRenderObjectWidget { ContainerRenderObjectWidget(this._containerSize, this.children) : super(children: RepaintBoundary.wrapAll(List.from(children))); @override final List children; Size get containerSize { return _containerSize; } Size _containerSize = Size.zero; @override RenderContainerBox createRenderObject(BuildContext context) { return RenderContainerBox(containerSize); } @override void updateRenderObject( BuildContext context, RenderContainerBox renderObject) { super.updateRenderObject(context, renderObject); renderObject..containerSize = containerSize; } } class RenderContainerBox extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { RenderContainerBox(this._containerSize); @override void setupParentData(RenderObject child) { super.setupParentData(child); if (child.parentData is! ContainerParentData) { child.parentData = ContainerParentData(); } } Size get containerSize => _containerSize; Size _containerSize = Size.zero; set containerSize(Size newContainerSize) { if (_containerSize == newContainerSize) { return; } _containerSize = newContainerSize; markNeedsLayout(); markNeedsPaint(); } @override bool get isRepaintBoundary => false; @override void performLayout() { size = constraints.constrain(Size(containerSize.width, containerSize.height)); void layout( {required RenderBox child, required double width, required double height}) { child.layout(BoxConstraints.tightFor(width: width, height: height), parentUsesSize: true); } RenderBox? child = firstChild; while (child != null) { final ContainerParentData parentData = child.parentData! as ContainerParentData; final RenderBox itemsDetails = child; if (itemsDetails is RenderRecord) { final Rect itemRect = Rect.fromLTWH(0, 0, containerSize.width, 50); parentData ..width = itemRect.width ..height = itemRect.height ..offset = Offset(0, itemsDetails.itemIndex.toDouble()); layout( child: child, width: parentData.width, height: parentData.height); } child = parentData.nextSibling; } } @override void paint(PaintingContext context, Offset offset) { RenderBox? child = firstChild; while (child != null) { final ContainerParentData childParentData = child.parentData! as ContainerParentData; final RenderBox itemsDetails = child; if (itemsDetails is RenderRecord) { if (childParentData.width != 0.0 && childParentData.height != 0.0) { context.paintChild( child, Offset(0, itemsDetails.itemIndex * 50) + offset); } } child = childParentData.nextSibling; } } } class ContainerParentData extends ContainerBoxParentData { double width = 0.0; double height = 0.0; } class RecordRenderWidget extends SingleChildRenderObjectWidget { RecordRenderWidget({required this.itemIndex, required this.child}) : super(child: RepaintBoundary.wrap(child, 0)); int itemIndex; @override final Widget child; @override RenderObject createRenderObject(BuildContext context) { return RenderRecord(itemIndex: itemIndex); } @override void updateRenderObject(BuildContext context, RenderRecord renderObject) { super.updateRenderObject(context, renderObject); renderObject.itemIndex = itemIndex; } } class RenderRecord extends RenderBox with RenderObjectWithChildMixin { RenderRecord({required this.itemIndex}); int itemIndex; @override bool get isRepaintBoundary => true; @override void performLayout() { size = constraints .constrain(Size(constraints.maxWidth, constraints.maxHeight)); if (child != null) { child!.layout( BoxConstraints.tightFor( width: constraints.maxWidth, height: constraints.maxHeight), parentUsesSize: true); } } @override void paint(PaintingContext context, Offset offset) { if (child != null) { context.paintChild(child!, offset); } super.paint(context, offset); } } class ItemsBuilder { List items = []; ItemsBuilder() { items = []; } void fetchItems(int startIndex, int endIndex) { items.forEach((item) => item.isEnsured = false); for (int index = startIndex; index <= endIndex; index++) { ItemsDetails? item = items.firstWhereOrNull((item) => item.itemIndex == index); if (item != null) { item.isEnsured = true; item.element = DataElementWidget(itemIndex: index); } else { item = items.firstWhereOrNull((item) => item.itemIndex == -1 || !(item.itemIndex >= startIndex && item.itemIndex <= endIndex)); if (item != null) { item.updateItemDetails(index); } if (item == null) { item = createNewItem(index); items.add(item); } } } items.forEach((item) { if (!item.isEnsured) { item.itemIndex = -1; } }); } ItemsDetails createNewItem(int itemIndex) { var item = ItemsDetails(); item.isEnsured = true; item.itemIndex = itemIndex; item.element = DataElementWidget(itemIndex: itemIndex); return item; } } class ItemsDetails { bool isEnsured = false; int itemIndex = -1; late Widget element; void updateItemDetails(int itemIndex) { this.itemIndex = itemIndex; this.isEnsured = true; this.element = DataElementWidget(itemIndex: itemIndex); } } class DataElementWidget extends StatefulWidget { const DataElementWidget({ Key? key, required this.itemIndex, }) : super(key: key); final int itemIndex; @override State createState() => _DataElementWidget(); } class _DataElementWidget extends State { @override Widget build(BuildContext context) { return RecordRenderWidget( itemIndex: widget.itemIndex, child: Container( color: Colors.blue, child: Text( 'Items Count: ${widget.itemIndex}\n', style: TextStyle(color: Color.fromRGBO(0, 0, 0, 0.87)), ), ), ); } } class ItemsContainer { static final ItemsContainer instance = ItemsContainer._(); factory ItemsContainer() => instance; ItemsContainer._() { data = ItemsBuilder(); } double verticalOffset = 0; Size? renderSize; late ItemsBuilder data; List getItemsStartEndIndex() { int startIndex = (verticalOffset ~/ 50).floor(); int endIndex = ((verticalOffset + 600) / 50).ceil(); return [startIndex, endIndex]; } ItemsBuilder fetchItemsDetails() { var indexes = getItemsStartEndIndex(); int startRowIndex = indexes[0], endRowIndex = indexes[1]; data.fetchItems(startRowIndex, endRowIndex); return data; } } ```

Screenshots or Video

Screenshots / Video demonstration https://github.com/flutter/flutter/assets/120440951/d392a6e2-930d-42ff-923d-be958fea691b

Logs

Logs ```console [Paste your logs here] ```

Flutter Doctor output

Doctor output ```console Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 3.22.2, on Microsoft Windows [Version 10.0.19045.4529], locale en-US) [√] Windows Version (Installed version of Windows is version 10 or higher) [√] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [√] Chrome - develop for the web [√] Visual Studio - develop Windows apps (Visual Studio Professional 2022 17.9.6) [√] Android Studio (version 2021.3) [√] VS Code (version 1.90.2) [√] Connected device (3 available) [√] Network resources • No issues found! ```
huycozy commented 3 months ago

Reproduced the issue as reported on Flutter master channel as well.

flutter doctor -v (stable and master) ```bash [✓] Flutter (Channel stable, 3.22.2, on macOS 14.1 23B74 darwin-x64, locale en-VN) • Flutter version 3.22.2 on channel stable at /Users/huynq/Documents/GitHub/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 761747bfc5 (3 weeks ago), 2024-06-05 22:15:13 +0200 • Engine revision edd8546116 • Dart version 3.4.3 • DevTools version 2.34.3 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/huynq/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/huynq/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.4) • Xcode at /Applications/Xcode15.4.app/Contents/Developer • Build 15F31d • CocoaPods version 1.15.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • android-studio-dir = /Applications/Android Studio.app/ • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160) [✓] VS Code (version 1.90.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.90.0 [✓] Connected device (3 available) • iPhone (mobile) • d9a94afe2b649fef56ba0bfeb052f0f2a7dae95e • ios • iOS 15.8 19H370 • macOS (desktop) • macos • darwin-x64 • macOS 14.1 23B74 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 126.0.6478.127 [✓] Network resources • All expected network resources are available. • No issues found! ``` ```bash [!] Flutter (Channel master, 3.23.0-14.0.pre.139, on macOS 14.1 23B74 darwin-x64, locale en-VN) • Flutter version 3.23.0-14.0.pre.139 on channel master at /Users/huynq/Documents/GitHub/flutter_master ! Warning: `flutter` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path. ! Warning: `dart` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/dart, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path. • Upstream repository https://github.com/flutter/flutter.git • Framework revision bf40ae3bc7 (6 hours ago), 2024-07-03 14:47:16 -0700 • Engine revision 4190543cb0 • Dart version 3.5.0 (build 3.5.0-323.0.dev) • DevTools version 2.37.0 • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades. [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/huynq/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/huynq/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.4) • Xcode at /Applications/Xcode15.4.app/Contents/Developer • Build 15F31d • CocoaPods version 1.15.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • android-studio-dir = /Applications/Android Studio.app/ • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160) [✓] VS Code (version 1.90.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.92.0 [✓] Connected device (3 available) • Pixel 7 (mobile) • 2B171FDH20084L • android-arm64 • Android 14 (API 34) • macOS (desktop) • macos • darwin-x64 • macOS 14.1 23B74 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 126.0.6478.127 [✓] Network resources • All expected network resources are available. ! Doctor found issues in 1 category. ```
Piinks commented 2 months ago

Hi @abineshPalanisamy thanks for reporting this! I am not sure if this is an issue with the framework, it looks like you have your own custom render object handling the layout.

The sample is also quite large, can you try to reduce the amount of code here so it is easier to identify where the issue is coming from? https://github.com/flutter/flutter/tree/master/docs/contributing/issue_hygiene#provide-reduced-test-cases

abineshPalanisamy commented 2 months ago

Hi @Piinks ,

We have already provided the sample with the required code changes and ensured the offsets and positions are correct. There is no issue with the layout. This is why we could only reproduce the issue when RepaintBoundary is set to true.

Piinks commented 1 month ago

Unfortunately it is not currently clear if this is an issue with the provided code or the framework. If it is at all possible to reduce the sample, let us know and we can take another look!