Skip to content

Conversation

xu-baolin
Copy link
Member

@xu-baolin xu-baolin commented Nov 3, 2020

Description

This is the minimal reproducible sample

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Center(
        child: SizedBox(
          width: 100,
          height: 100,
          child: Table(
            children: [
              TableRow(children: [W()])
            ],
          ),
        ),
      ),
    ),
  );
}

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

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

class _State extends State<W> {
  void _onTap() {
    setState(() => _toggle = !_toggle);
  }

  bool _toggle = true;
  @override
  Widget build(BuildContext context) {
    return _toggle
        ? GestureDetector(onTap: _onTap, child: Text('Tap me'))
        : SizedBox(child: Text('Nice.'));
  }
}

After Tapping Tap me, the new RenderObject of SizeBox was not adopted by the RenderTable

How does it happen?

  1. The onTap callback is called, _State.context is marked dirty.
  2. In the next build pass, because GestureDetector was replaced by a widget of a different type, _State.context.updateChild method calls _State.context.deactivateChild.
  3. The child's renderObject asks _TableElement to detach it from _TableElement.renderObject, calling _TableElement.removeChildRenderObject.
  4. The old RenderObject was droped by renderObject.setChild(childParentData.x!, childParentData.y!, null).
  5. The new type RenderObject of Text asks _TableElement to attach it to _TableElement.renderObject, calling _TableElement.insertRenderObjectChild, but this method do not let _TableElement.renderObject adopt it.
  6. Currently, the child rederObject can only be adopted at _TableElement.mount and _TableElement.update, therefore, in the above scenario, the new RO has no chance to be adopted.

My solution

  1. Adjust the actions of adopting children to _TableElement.insertRenderObjectChild.
  2. Inflate _TableElement children with IndexedSlot so that we know where to insert when we adopt a child in _TableElement.insertRenderObjectChild.

Related Issues

Fixes #69395

Tests

I added the following tests:

See files.

@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label Nov 3, 2020
@google-cla google-cla bot added the cla: yes label Nov 3, 2020
@@ -595,7 +595,7 @@ class RenderTable extends RenderBox {
/// children are simply moved to their new location in the table rather than
/// removed from the table and re-added.
void setFlatChildren(int columns, List<RenderBox?> cells) {
if (cells == _children && columns == _columns)
if (listEquals(cells, _children) && columns == _columns)
Copy link
Member Author

@xu-baolin xu-baolin Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an optimization of duplicate check logic
The List operator == only equal to themselves.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you verified that this is actually faster? It trades a pointer comparison for an O(N) comparison during updates to avoid an O(N) cost in layout, but it's not clear to me how common it would be for the child list to be a new instance but holding the same contents.

Copy link
Member Author

@xu-baolin xu-baolin Nov 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the caller of setFlatChildrenalways generate a new List instant so this is a dead code never reachable.

Next, let's analyze different scenarios:

  1. Update TableRow widgets without changing its Type, so the cells parameter will have the same renderObjects and Will achieve optimized results.
  2. Change the TableRow widgets count will have no effect because listEquals will compares length first.
  3. Update ``TableRow` widgets with a different renderObjectWidget and keep the same count,like you said will increase the time complexity.

In summary, I believe that the benefits of such a modification are greater than the costs, Of course, also need to listen to your suggestions :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we always call with a new list, then this is converting an O(1) equality test with an O(N) equality test, which is slower, in order to avoid the code below, which is O(N) (but when avoided becomes O(1), so faster). This seems like it should be better... but my question is, is it?

The way to find out would be to benchmark it.

@xu-baolin
Copy link
Member Author

@Hixie hi,
Please review,thanks

@Hixie
Copy link
Contributor

Hixie commented Nov 4, 2020

cc @goderbauer who is more familiar with IndexedSlot.

@goderbauer goderbauer self-requested a review November 6, 2020 00:39
@xu-baolin
Copy link
Member Author

@goderbauer Hi, Would you like to take a look at this patch? Thanks very much :)

}

@override
void insertRenderObjectChild(RenderObject child, dynamic slot) {
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that you don't really need an IndexedSlot here. Wouldn't it be more convenient to just define a _TableSlot with an x and y property representing the row/column of the child?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If we do that, I am wondering whether we still need the TableCellParentData, which contains the same information and appears to only be used in removeRenderObjectChild)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this bug scenario, we must use IndexedSlot, we need the position info to decide where to insert the new RO.

@@ -292,21 +292,34 @@ class _TableElement extends RenderObjectElement {
@override
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 287 about slots not being used seems outdated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

? (widget.children[0].children != null ? widget.children[0].children!.length : 0)
: 0;
final int _index = slot.index;
renderObject.setChild(_index % _columns, _index ~/ _columns, child as RenderBox);
}

@override
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that slots have meaning, do we need an implementation for moveRenderObjectChild?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, Under your reminder, I found some bugs, the current implementation is a bit strange, I need to continue working on this.

@xu-baolin
Copy link
Member Author

Currently, the Tabel widget extends form RenderObjectWidget, But I think it is more suitable to extend from MultiChildRenderObjectWidget,
@Hixie, Do you remember your thoughts on doing this?

@xu-baolin
Copy link
Member Author

IndexedSlot data are sometimes unreliable, because updateChildren() is called multiple times in batches by _TableElement.update, which causes the IndexedSlot modified incorrectly.

It seems that we need to refactor _TableElement class to solve this issue perfectly.

@goderbauer @Hixie Do you guys have some thoughts to resolve this issue?

@xu-baolin
Copy link
Member Author

The root cause of this issue in one sentence is that when only the subtree below Table changes and needs to update the Table RO children, the new RO is not adopted. Please see the description above for details.

@xu-baolin
Copy link
Member Author

This update has made a big change to RenderTable. This class has many APIs developed for testing RO, for example, RenderTable.children, addRow(), setFlatChildren, setChildren, setChild, etc., many of these interfaces I think are designed for testing ( If my understanding is wrong, please correct it. If we accept this solution, these interfaces will be deleted (I am not sure if this is considered a breaking change) and then rewrite the RO test cases. If these APIs cannot be abandoned, I have not thought of a better solution to solve this bug perfectly.

@xu-baolin
Copy link
Member Author

Please ignore the test case I temporarily commented out

@xu-baolin xu-baolin changed the title Fix a Table Widget Render bug Refactor the RenderTable class Dec 15, 2020
@xu-baolin xu-baolin requested a review from goderbauer December 15, 2020 07:39
@@ -376,87 +376,34 @@ class RenderTable extends RenderBox {
ImageConfiguration configuration = ImageConfiguration.empty,
TableCellVerticalAlignment defaultVerticalAlignment = TableCellVerticalAlignment.top,
TextBaseline? textBaseline,
List<List<RenderBox>>? children,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove children here? Most RenderObjects with children have an option to specify them in the constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: update the doc comment above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comes back again.

int get columns => _columns;
int _columns;
set columns(int value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why's there no setter for columns/rows anymore? Since it is a constructor argument, you should be able to change it after the object is constructed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be changed by setDimensions

if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew]))
lostChildren.add(_children[xyOld]!);
}
/// If `after` is null, then this inserts the child at the start of the list.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it sound as if there's only one dimension to the children. However, it's not a linear list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe expand the comment to describe what the model for the children is here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or can we change this to an API that's more explicit about the actual child model, maybe something like:

void insertChild(RenderBox child, {required int row, required int column});

Since the element has access to the index of the child, could it use that to calculate what column and row the child needs to be in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comment below.

Copy link
Member Author

@xu-baolin xu-baolin Dec 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, _children is variable length, use row or column is not suitable.

_rows = 0;
cells.forEach(addRow);
/// Update the number of row and column of this table.
void updateInfo(int rows, int columns) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this name is very generic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call this setDimensions? And move it closer to the column and row getter? Also update the docs of those getters to link to this method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also document that all the children need to be inserted first before calling this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

/// Adds a row to the end of the table.
/// Move the given `child` in the child list to be after another child.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as on insertChild.

for (final TableRow row in newWidget.children) {
List<Element> oldChildren;
if (row.key != null && oldKeyedRows.containsKey(row.key)) {
oldChildren = oldKeyedRows[row.key]!;
taken.add(oldChildren);
} else if (row.key == null && oldUnkeyedRows.moveNext()) {
} else if (oldUnkeyedRows.moveNext()) {
oldChildren = oldUnkeyedRows.current.children;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe turn the removed row.key == null condition into an assert here.

Copy link
Member Author

@xu-baolin xu-baolin Dec 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that add a new keyed row here,
It’s a bit difficult to handle here because we have to ensure that one-time update of children.

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Center(
        child: SizedBox(
          width: 100,
//          height: 100,
          child: W(),
        ),
      ),
    ),
  );
}

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

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

class _State extends State<W> {
  void _onTap() {
    setState(() => _toggle = !_toggle);
  }

  List<TableRow> _getRows() {
    if (_toggle) {
      return [
        TableRow(key: Key('1'), children: [Text('ke1')]),
        TableRow(children: [Text('unkey')]),
        TableRow(key: Key('2'), children: [Text('ke2')]),
      ];
    } else {
      return [
        TableRow(key: Key('2'), children: [Text('ke2')]),
//        TableRow(key: Key('1'), children: [Text('ke1')]),
        TableRow(children: [Text('unkey')]),
        TableRow(key: Key('3'), children: [Text('ke3')]),
      ];
    }
  }

  bool _toggle = true;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        GestureDetector(onTap: _onTap, child: Text('Tap me')),
        Table(
          children: _getRows(),
        )
      ],
    );
  }
}

For the code above, I think the old key1 should be dropChild, and then the new key3 should be adoptChild.
But our current processing is to reuse the old RO and I think there will be no problem with this processing(the real RO(Text) can be reused), what do you think?

Probably the TableRow should not have a local key I think.

}
while (oldUnkeyedRows.moveNext())
allOldChildren.addAll(oldUnkeyedRows.current.children);
oldKeyedRows.values.where((List<Element> list) => !taken.contains(list)).forEach(allOldChildren.addAll);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably do this smarter since we know what has been taken. Maybe above in line 412/413 instead of adding them to the extra "taken" list just remove them from oldKeyedRows, so in the end oldKeyedRows contains everything that's not taken?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

assert(columns * rows == updatedChildren.length);

int rowIndex = 0;
for (final TableRow row in newWidget.children) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since row isn't used in this for loop maybe just fo for (int row = 0; row < rows; row++)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

lostChildren.add(_children[xyOld]!);
}
/// If `after` is null, then this inserts the child at the start of the list.
void insertChild(RenderBox? child, { RenderBox? after }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allow child to be null? What does it mean here to insert a null-child?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit test before this change inserts a lot of null-child into _children, Which is not allowed now.

Comment on lines 82 to 83
table.insertChild(null);
table.insertChild(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd, what does this mean?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(here and elsewhere)

RenderTable table;
layout(RenderPositionedBox(child: table = RenderTable(
columns: 5,
rows: 5,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer allow the existence of null RO for _children

@@ -121,52 +132,225 @@ void main() {
' │ size: Size(100.0, 10.0)\n'
' │ additionalConstraints: BoxConstraints(w=30.0, h=10.0)\n'
' │\n'
' ├─child (3, 0) is null\n'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer allow the existence of null RO for _children

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't that a problem?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to #69670 (comment)

@xu-baolin xu-baolin requested a review from goderbauer December 16, 2020 07:06
@xu-baolin
Copy link
Member Author

@goderbauer Hi, please review this if you have time, thanks.

@xu-baolin
Copy link
Member Author

xu-baolin commented Jan 26, 2021

It has been more than a month since the last review, try ping again, in case someone receives the message and just has time!

@Hixie
Copy link
Contributor

Hixie commented Jan 27, 2021

Sorry about the delay. This is a complicated PR and reviewing it will take time, so we've all been delaying until we have the time. I understand that this can be frustrating.

@@ -348,7 +348,7 @@ void main() {
expect(boxA2, isNotNull);
expect(boxG2, isNotNull);
expect(boxA1, equals(boxA2));
expect(boxG1, isNot(equals(boxG2)));
expect(boxG1, equals(equals(boxG2)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems concerning, there shouldn't be any logic changes right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aah, i understand now. this is literally the change this PR is about. Sorry I misunderstood the test before.

@Hixie
Copy link
Contributor

Hixie commented Jan 28, 2021

many of these interfaces I think are designed for testing ( If my understanding is wrong, please correct it. If we accept this solution, these interfaces will be deleted (I am not sure if this is considered a breaking change)

Those APIs are intended for people who directly use the RenderTable API without going through widgets.

@Hixie
Copy link
Contributor

Hixie commented Jan 28, 2021

If the goal is just to make RenderTable handle changes of child render objects correctly, I wouldn't expect so many changes on the tests, just new tests that check the behavior that is fixed. Can you elaborate on what the changes are about?

@xu-baolin
Copy link
Member Author

Okay, no problem. I will organize the language and will reply to you ASAP.
At the same time, you can get some context from the comments.

@@ -348,39 +348,44 @@ class _TableElement extends RenderObjectElement {
@override
RenderTable get renderObject => super.renderObject as RenderTable;

// This class ignores the child's slot entirely.
// Instead of doing incremental updates to the child list, it replaces the entire list each frame.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hixie
It’s weird to deal with it this way. We should not recreate(drop and new create) the cell elements because their keys and types have not changed, right?
For example, We should not rebuild element and RO in the following situation,which you concerning
https://github.com/flutter/flutter/pull/69670/files/60b124b9a86717f4e0aeaf203e23c8d0c1e3944e#r565894797
before rebuild: [Text('A'), Text('B'), Text('C'), Text('D'), Text('E'), ] with (column:3, row:2)
after update: [Text('a'), Text('b'), Text('c'), Text('d'), Text('e'), ] with (column:2, row:3)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore the child slot is the root cause of the related issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree that it would be better to not replace the elements in that case

}).toList(growable: false),
);
}).toList(growable: false);
_updateRenderObjectChildren();
Copy link
Member Author

@xu-baolin xu-baolin Jan 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the slot is introduced, we can't add RO children here uniformly, we can only add children to RO through void insertRenderObjectChild.

This is exactly why we need to refactor the render. I don't want to modify it on a large scale, but we have to. RenderTable is really different from other similar multi-child RO implementations. I don't know if the author at the time had a special purpose. My modification is to make its implementation not so special, and then the issue is solved naturally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xu-baolin
Copy link
Member Author

xu-baolin commented Jan 28, 2021

@Hixie Hi,
I described the reason why I did this, please see the latest comments. I do not confirm that my description can make you understand quickly, because my English expression is not particularly good, please forgive me, if you are unclear, please let me know.
In addition, I am very confident that my modification is reasonable. You can also try to be independent of my code and find a solution to fix this bug and the problem of rebuilding the RO mentioned above. I believe you will do the same. :)

@xu-baolin
Copy link
Member Author

Also needs @goderbauer 's thoughts :)

allOldChildren.addAll(oldUnkeyedRows.current.children);
oldKeyedRows.values.forEach(allOldChildren.addAll);

final List<Element> updatedChildren = updateChildren(allOldChildren, allNewChildren, forgottenChildren: _forgottenChildren);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to use the slot index, we must update all the children (call updateChildren()) one-time, otherwise, the calculation of the slot is wrong.

This is why make the change here.

@Hixie
Copy link
Contributor

Hixie commented Jan 28, 2021

I don't know when I'll have the time to study this carefully enough to review it, sorry. I really need to just walk through the code locally to figure out what's going on, and that will need more time than I have available at the moment. (If @goderbauer has the time and reviews this PR then I'm happy to defer to his expertise, don't feel you must get my review.) I'll keep this tab open though, in case I have some time available.

@Hixie
Copy link
Contributor

Hixie commented Jan 29, 2021

(maybe we can trade reviews, though, on our new #review-trading-floor channel!)

@xu-baolin
Copy link
Member Author

I'll post this on the channel after I finish review someone else's pr. It's a little embarrassing to keep making requests.

if (children != null) {
final List<RenderBox> allChildren = <RenderBox>[];
for (final List<RenderBox> rows in children)
rows.forEach(allChildren.add);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do allChildren.addAll(rows)?

children?.forEach(addRow);
if (children != null) {
final List<RenderBox> allChildren = <RenderBox>[];
for (final List<RenderBox> rows in children)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows makes it sound like each entry in children is multiple rows, but if i understand correctly, they are each a single row, so row would be more appropriate

///
/// Changing the number of columns is an expensive operation because the table
/// needs to rearrange its internal representation.
/// This can be updated by [setDimensions] after updating children is finish.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what "is finish" means here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(repeated other times below)

assert(value >= 0);
if (value == rows)

/// Update the number of row and column of this table.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

number of rows and columns of...

_rows = value;
_children.length = columns * rows;
_columns = columns;
_rows = rows;
markNeedsLayout();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem to handle _children being too long compared to the number of rows and columns any more

/// If the new cells contain any existing children of the table, those
/// children are simply moved to their new location in the table rather than
/// removed from the table and re-added.
void setFlatChildren(int columns, List<RenderBox?> cells) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a chance that this and some of the other API changes will break someone. I would feel much more comfortable if the API was backwards compatible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the older API (setFlatChildren, addRow, setChild at x,y) is also potentially much more efficient than the new API

markNeedsLayout();
return;
/// If `after` is null, then this inserts the child at the start of the list.
/// Children are stored in row-major order, you should call [setDimensions]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// If `after` is null, then this inserts the child at the start of the list.
/// Children are stored in row-major order, you should call [setDimensions]
/// after updating children is finish to set the table dimensions.
void insertChild(RenderBox child, { RenderBox? after }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to say in the documentation here that using the after argument is extremely expensive (potentially O(N^2) depending on how List is implemented, and potentially O(N^3) if used in a loop, which seems like a common usage).

Unless there's a really strong reason for having after (I haven't checked the rest of the PR yet) it might be better to just not have that argument available.

for (final List<RenderBox> rows in children)
rows.forEach(allChildren.add);

allChildren.reversed.forEach(insertChild);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that all that this does is insert(0) into _children, why not just add all the children to _children directly instead of adding to a local variable and then reversing the list (expensive) and then inserting all the children into the other list (expensive)?

adoptChild(cell);
/// More efficient than removing and re-adding the child. Requires the child
/// to already be in the child list at some position. Pass null for `after` to
/// move the child to the start of the child list.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's more efficient, but it's still really expensive (potentially O(N) for the remove, O(N) for the insert, O(N) for the contains assert, O(N) for the indexOf...)

@Hixie
Copy link
Contributor

Hixie commented Feb 18, 2021

I'm not really convinced this is the right approach here. I agree with the goal of fixing the bug described in the original PR description. But I think actually the test where expect(boxG1, isNot(equals(boxG2))); is actually correct as it is now on master, not as it is in this PR. That is, I don't think we should be trying to match keys at different coordinates.

I think the right solution here is for the Table to use slots with X/Y coordinates so that insertRenderObjectChild can correctly place the new child in the right place, without needing to change anything in the render object.

@xu-baolin
Copy link
Member Author

re-work at #76375

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Table cell content does not render if FutureBuilder returns widget of different type when state changes
3 participants