Skip to content

Fixed onSort bug in data_table.dart#43735

Closed
andermoran wants to merge 2 commits intoflutter:masterfrom
andermoran:master
Closed

Fixed onSort bug in data_table.dart#43735
andermoran wants to merge 2 commits intoflutter:masterfrom
andermoran:master

Conversation

@andermoran
Copy link

@andermoran andermoran commented Oct 29, 2019

Problem

A bad comparison argument on a ternary operator made it impossible for a null value to be passed into _buildHeadingCell's onSort parameter causing alignment problems described in the related issues referenced below.

Description

Short explanation of my fix

So for the line

onSort: () => column.onSort != null ? column.onSort(dataColumnIndex, sortColumnIndex != dataColumnIndex || !sortAscending) : null

the comparison argument is column.onSort != null instead of () => column.onSort != null. That is where the problem lies. Because the onSort parameter will either take () => column.onSort(dataColumnIndex, sortColumnIndex != dataColumnIndex || !sortAscending) or () => null and BOTH are non-null VoidCallback functions.
If you remove the () => from in front of the comparison argument, you fix that problem; however, you still need to return a callback function or null so change the second argument to () => column.onSort(dataColumnIndex, sortColumnIndex != dataColumnIndex || !sortAscending) so that is passes a VoidCallback function.

Long explanation of my fix

typedef VoidCallback = void Function();

void myFunction({VoidCallback myCallback}) {
  if (myCallback != null) {
    print("myCallback is NOT null. Calling myCallback now.");
    myCallback();
  } else {
    print("myCallback is null!");
  }
}

void main() {
  VoidCallback callback0;
  VoidCallback callback1 = () => print("callback1");

  /* This is how it should be
   * -------------------------------------------
   * We check if the callback0 has been defined.
   * If it has, then we pass
   * 
   * callback0
   * 
   * as the parameter, if not, we pass
   * 
   * null
   * 
   * as the parameter.
   */
  myFunction(myCallback: callback0 != null ? callback0 : null);

  /* This is the logic data_table.dart implements
   * -------------------------------------------
   * We check if the callback0 has been defined.
   * If it has, then we pass
   * 
   * () => callback0
   * 
   * as the parameter, if not, we pass
   * 
   * () => null
   * 
   * as the parameter.
   * 
   * Regardless of whether callback0 is null or not,
   * this results in a non-null VoidCallback function
   * being passed as the parameter because both
   * 
   * () => callback0
   * 
   * and 
   * 
   * () => null
   * 
   * are non-null VoidCallback functions because
   * they are defined with that line.
   * 
   * This results in the if statement
   * 
   * if (onSort != null) [this is currently on line 435 of data_table.dart]
   * 
   * always evaluating to true.
   */
  myFunction(myCallback: () => callback0 != null ? callback0 : null);
}

Related Issues

Tests

I didn't add any tests as this is already a part of Flutter's code and is an incredibly simple fix.

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (Please read Handling breaking changes). Replace this with a link to the e-mail where you asked for input on this proposed change.
  • No, this is not a breaking change.

A comparison argument always returned true and caused the DataColumn to always think the onSort parameter (which is optional) was provided even when it wasn't.
@fluttergithubbot fluttergithubbot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Oct 29, 2019
@fluttergithubbot
Copy link
Contributor

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here with @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@andermoran
Copy link
Author

andermoran commented Oct 29, 2019

@Hixie do I need any tests for a change so straightforward?

cc @willlarche

@andermoran
Copy link
Author

@googlebot I signed it!

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@HansMuller
Copy link
Contributor

This appears to be a nice fix for what was probably a typo; thanks! It will definitely need a test: we don't want this issue to crop up again.

Also: there are some presubmit DataTable test failures.

@andermoran
Copy link
Author

@HansMuller I've never done a pull request on a public repository so I'm unsure what the next steps are to get this approved.

@willlarche
Copy link
Contributor

You’re doing a great job!

Find a test file that’s already doing tests on these classes and add one specifically to check for your case that is being fixed.

@andermoran
Copy link
Author

andermoran commented Nov 1, 2019

What I want to test:
If you create a DataTable with DataColumns that have no onSort parameter, the text in the cells will not be centered with the columns and the text of the columns will be slightly misaligned because flutter thinks it needs to have space for the arrow associated with a column when the user taps on the column to sort it.
Misaligned columns

My fix corrects this (as shown below) and automatically centers the content in the column but I have not the slightest idea as to how to write a test for this.
image

I know the test file is flutter/packages/flutter/test/material/data_table_test.dart and the test should be in the existing test

  testWidgets('DataTable column onSort test', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: DataTable(
            columns: const <DataColumn>[
              DataColumn(
                label: Text('Dessert'),
              ),
            ],
            rows: const <DataRow>[
              DataRow(
                cells: <DataCell>[
                  DataCell(
                    Text('Lollipop'), // wraps
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
    await tester.tap(find.text('Dessert'));
    await tester.pump();
    expect(tester.takeException(), isNull);
  });

I somehow need to test the _buildHeadingCell method of DataTable to make sure that it is possible for a null value to be passed into the onSort parameter. I am unsure how to test a private method in a test file.

@andermoran
Copy link
Author

Also, how is this test

  testWidgets('DataTable column onSort test', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: DataTable(
            columns: const <DataColumn>[
              DataColumn(
                label: Text('Dessert'),
              ),
            ],
            rows: const <DataRow>[
              DataRow(
                cells: <DataCell>[
                  DataCell(
                    Text('Lollipop'), // wraps
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
    await tester.tap(find.text('Dessert'));
    await tester.pump();
    expect(tester.takeException(), isNull);
  });

testing onSort? There isn't even an onSort parameter passed so what exactly is this testing for?

If onSort is false, we still need to assign the label to a Row widget.
@andermoran
Copy link
Author

andermoran commented Nov 1, 2019

Note: another bad ternary comparison argument is also present in this snipped of code in data_table.dart but I am unaware of its side effects.

      for (DataRow row in rows) {
        tableRows[rowIndex].children[0] = _buildCheckbox(
          color: theme.accentColor,
          checked: row.selected,
          onRowTap: () => row.onSelectChanged != null ? row.onSelectChanged(!row.selected) : null ,
          onCheckboxChanged: row.onSelectChanged,
        );
        rowIndex += 1;
      }

@andermoran
Copy link
Author

@HansMuller I'm stuck here and want to get this fix implemented as soon as possible! I'm struggling on the new tests I'm supposed to create (even the existing tests don't make sense) and I need some help.

@andermoran
Copy link
Author

@HansMuller I believe this pr is ready

@HansMuller
Copy link
Contributor

@andermoran the PR still doesn't include a test. I'm afraid I don't have the time to help craft one at the moment, sorry about that.

@andermoran
Copy link
Author

Despite weeks of looking at several resources to figure out how to write flutter tests, I still can't get the hang of it. Will this bug fix ever be merged?

@Hixie
Copy link
Contributor

Hixie commented Jan 10, 2020

@andermoran
Copy link
Author

@Hixie I’m not sure if you’re trolling me or not but I just said I’ve spent weeks looking at several resources to write flutter tests but have been unable to. Yes, I have looked at the intro page as well as many other resources. I’ve really tried my hardest to write these tests because I want this merge to happen badly but I’ve been unable to. I’ve looked at all the resources on the web about flutter testing as well as looking through flutter tests in flutter’s source code to how it was done there. At this point I’ve given up. I wish someone could at least write one test for this PR that I need so that I can work off that.

@Hixie
Copy link
Contributor

Hixie commented Jan 11, 2020

If you could file a bug that describes what you tried and how the documentation failed you, that would be great. I'm sad to hear that the current documentation was inadequate to the task.

@deadsoul44
Copy link

Any update?

@dkwingsmt
Copy link
Contributor

@andermoran I looked into it since this is indeed an interesting PR and exposes some problems of our code coverage. I came up with a unit test as below:

  testWidgets('DataTable column label position - no sort', (WidgetTester tester) async {
    Widget buildTable({ int sortColumnIndex, bool sortAscending = true }) {
      return DataTable(
        sortColumnIndex: sortColumnIndex,
        sortAscending: sortAscending,
        columns: <DataColumn>[
          const DataColumn(
            label: Expanded(child: Center(child: Text('Name'))),
            tooltip: 'Name',
          ),
          DataColumn(
            label: const Expanded(child: Center(child: Text('Calories'))),
            tooltip: 'Calories',
            numeric: true,
            onSort: (int columnIndex, bool ascending) {},
          ),
        ],
        rows: const <DataRow>[
          DataRow(
            cells: <DataCell>[
              DataCell(Text('A long desert name')),
              DataCell(Text('A lot of calories')),
            ],
          ),
        ]
      );
    }

    await tester.pumpWidget(MaterialApp(
      home: Material(child: buildTable()),
    ));

    final Finder nameText = find.text('Name');
    expect(nameText, findsOneWidget);
    final Finder nameRow = find.ancestor(of: find.text('Name'), matching: find.byType(Row)).first;
    expect(tester.getCenter(nameText), equals(tester.getCenter(nameRow)));
  });

This test finds the text and its immediately wrapping row, and see if they have the same center, indicating that no other padding is added. This test breaks on master and passes on your PR.

However, I did find a bug (unrelated to this PR) that causes the header to disappear on mouse move, which I've filed at #51152. This bug seems to occur only when the text is not wrapped in the InkWell (which is only found out thanks to your PR), so until that issue is fixed, you'll need to wrap the header in InkWell unconditionally.

Hopefully this is a good starting point for you!

@deadsoul44
Copy link

I fear @andermoran gave up due its frustration. One of team members from flutter should finalize the pull request. It has been already more than 5 months.

@andermoran
Copy link
Author

I fear @andermoran gave up due its frustration. One of team members from flutter should finalize the pull request. It has been already more than 5 months.

Can confirm. Was frustrating to create a test. Did give up.

} else {
label = Row(
textDirection: numeric ? TextDirection.rtl : null,
children: <Widget>[ label ],
Copy link
Contributor

Choose a reason for hiding this comment

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

having a row with just one child seems unnecessarily expensive... is this just to make sure that you can toggle onSort without losing state in the child? I guess that makes sense...

Probably better in that case to do this as one expression, with UI-as-code.

Copy link
Author

Choose a reason for hiding this comment

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

I see your point. I debated whether to create a whole row for just one element. I went prioritized consistency over optimization. I would have no problem if that piece of code was changed, your point is valid

@Hixie
Copy link
Contributor

Hixie commented Feb 22, 2020

@dkwingsmt If the original bug was alignment issues we should probably have the test toggle the sortability (have onSort be null once, then non-null) and verify that the alignment is correct in both cases.

@dkwingsmt
Copy link
Contributor

@Hixie I can test that the alignment with onSort is consistent, but it's never gonna be at the center. (Although I can test that the row is centered in the cell, it's not useful since the row is supposed to fill up the cell).

@deadsoul44
Copy link

I would like to have datacolumn label to be still centered even if it is sorted (up or down arrow appears on the right of the centered label). It can be annoying to see centered label moves when it is sorted.

@dkwingsmt
Copy link
Contributor

dkwingsmt commented Feb 27, 2020

@deadsoul44 It's a good proposal, but let me explain the obstacle. The current structure can roughly be presented as:

tableCell = Container(
  child: Row(
    children: [
      label,  // The `label` argument of `DataColumn`
      if (onSort != null) sortingArrow,
    ],
  ),
);

Imagine the cell is 100px wide, the arrow being 10px wide and the text 40 px wide. You would expect that:

  1. If label: Text('Cities') the text is at the left edge of the cell, i.e. [0, 40]
  2. If label: Center(child: Text('Cities')) the text is at the center of the cell, [30, 70]
  • This wouldn't directly work because Row prefers the minimum size, but you can fix it by wrapping it with an Expand.
  1. If label: Align(alignment: Alignment.centerRight), child: Text('Cities')) the text is at the right edge of the possible space, i.e. [50, 90]. (You'll also need Expand).

Apparently No. 2 wouldn't center the text in the cell with the current implementation, because the text will be at the center of the remaining space, which is [25, 65].
Alright, what if we add a mirroring padding at the left, so that since we know the arrow is 10px wide, make the text centered within [10, 90], which is [30, 70]? Well then No. 1 would place the text at [10, 50] instead of the desired [0, 40].

The root cause is that DataTable have no information of its child label, and can't decide what to align its child to. I'd like to hear if you have any suggestions!

@dkwingsmt
Copy link
Contributor

I submitted a PR that continues this one in #51667

@deadsoul44
Copy link

deadsoul44 commented Feb 29, 2020

I understand that it is hard to align with current structure. Another option can be to add a "labelAlignment" argument. Depending on developer choice, the label can be right, center or left aligned. Currently, it is already assumed as right aligned if column is numeric and left aligned otherwise.

@deadsoul44
Copy link

Pending reviewer for two weeks...

@dkwingsmt
Copy link
Contributor

This PR has been continued by #51667, which has been merged.

@dkwingsmt dkwingsmt closed this Mar 13, 2020
@lock
Copy link

lock bot commented Apr 2, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@lock lock bot locked and limited conversation to collaborators Apr 2, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants