Skip to content

Fixes issue, ReorderableListView destroys children even if their key-type wasn't changed#64855

Merged
fluttergithubbot merged 18 commits intoflutter:masterfrom
haeseoklee:fix_reorderable_list_view
Sep 9, 2020
Merged

Fixes issue, ReorderableListView destroys children even if their key-type wasn't changed#64855
fluttergithubbot merged 18 commits intoflutter:masterfrom
haeseoklee:fix_reorderable_list_view

Conversation

@haeseoklee
Copy link
Contributor

@haeseoklee haeseoklee commented Aug 29, 2020

Description

GlobalObjectKey used in the child of ReorderableListView does not work well for key comparison.

I solved the problem by assigning a new Key called GlobalObjectValueKey.

It compares values within Objects rather than using identical.

example code
import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {

  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(title: 'ReorderableListView issue demo',),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  final List<int> data = List.generate(10, (index) => index);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: [_buildRefreshBtn()],
      ),
      body: Center(
          child: ReorderableListView(
            children: _buildTiles(),
            onReorder: (_, __) {},
          )
      ),
    );
  }

  Widget _buildRefreshBtn() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20.0),
      child: IconButton(
          icon: Icon(Icons.refresh),
          onPressed: (){setState(() {});}
      ),
    );
  }

  List<Widget> _buildTiles() {
    List<Widget> tiles = data.map((int val) {

      return MyListTile(
        key: ValueKey(val),
        data: val,
        title: "tile $val",
      );
    }).toList();
    return tiles;
  }
}


class MyListTile extends StatefulWidget {
  final int data;
  final String title;


  MyListTile({this.data, this.title, Key key}): super(key: key);

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


class _MyListTileState extends State<MyListTile> {
  Stream<int> _msStream;
  int _curMs;

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

    print("init: ${widget.title}");
    _curMs = 0;
    _msStream = Stream.periodic(const Duration(milliseconds: 100), (ms) => ms);

  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 30.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text("${widget.title}"),
          StreamBuilder<int>(
              stream: _msStream,
              initialData: _curMs,
              builder: (_, AsyncSnapshot<int> snapshot) {
                if (!snapshot.hasData) return Center(child: CircularProgressIndicator());

                String ms = "${snapshot.data} ms";
                return Text(ms);
              }
          )
        ],
      ),
    );
  }

  void dispose(){
    print("dispose: ${widget.title}");
    super.dispose();
  }
}

Here is a simplified reproduction written by @Hixie

example code2
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: OuterTest()));
}

class OuterTest extends StatelessWidget {
  OuterTest({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ReorderableListView(
      children: <Widget>[ InnerTest(key: ValueKey(0)) ],
      onReorder: (_, __) {},
    );
  }
}

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

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

class _InnerTestState extends State<InnerTest> {
  @override
  Widget build(BuildContext context) {
    return Text('$hashCode - should not change in hot reload');
  }
}

Related Issues

Fixes #58364

Tests

I added the following tests:

348475c

I referred to the code in the issue written by PixelToast. Thanks to @PixelToast.

This test can check whether the child key has changed when the widget is rebuilt.

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 read the Tree Hygiene wiki page, which explains my responsibilities.
  • 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

Did any tests fail when you ran them? Please read Handling breaking changes.

  • No, no existing tests failed, so this is not a breaking change.

@flutter-dashboard flutter-dashboard bot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Aug 29, 2020

@override
int get hashCode => super.hashCode;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The problem with this approach is it'll fail if there's two ReorderableListViews with items that have the same local keys. I think the solution is that _GlobalObjectValueKey should have two values, one is the key we're proxying to, and the other is the _ReorderableListContentState to which they belong. We can probably rename it to _ReorderableListChildGlobalKey or something, with arguments final Key subkey and final _ReorderableListContentState state or something like that, to make it clearer what is going on.

Copy link
Contributor Author

@haeseoklee haeseoklee Aug 30, 2020

Choose a reason for hiding this comment

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

@Hixie
That's a great idea! Thanks for your comment. I will apply it immediately.

Copy link
Member

@xu-baolin xu-baolin Sep 7, 2020

Choose a reason for hiding this comment

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

@Hixie Add _ReorderableListContentState as a value of the key can not solve the situation where multiple items use the same local key, they have the same _ReorderableListContentState, which will lead to Multiple widgets used the same GlobalKey, such as:

  @override
  Widget build(BuildContext context) {
    return ReorderableListView(
      children: <Widget>[ InnerTest(key: ValueKey(0)), InnerTest(key: ValueKey(0)) ],
      onReorder: (_, __) {},
    );
  }

The reason why there is no problem here is its wrong implementation of int get hashCode => identityHashCode(subKey);

Probably we could add the index into the values together to solve this problem.

I think we should add this to the unit test case.

Copy link
Member

Choose a reason for hiding this comment

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

@xu-baolin Two children can never have the same key, so the current behavior is correct

Copy link
Member

Choose a reason for hiding this comment

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

See: https://api.flutter.dev/flutter/foundation/Key-class.html

"Keys must be unique amongst the Elements with the same parent."

Copy link
Member

Choose a reason for hiding this comment

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

I see, thank you.

Copy link
Member

@xu-baolin xu-baolin Sep 7, 2020

Choose a reason for hiding this comment

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

@PixelToast However, this change not an equivalent modification, and change the behavior of fault tolerance.
Can we consider producing globalkey only through state and index,so that we don't have to force developers to provide the widget with a key.

Copy link
Member

Choose a reason for hiding this comment

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

@xu-baolin No problem!

This PR is for fixing a specific bug and not extending functionality though, you are welcome to open a new issue to discuss any changes to functionality that would be nice to have.

@Hixie
Copy link
Contributor

Hixie commented Aug 29, 2020

Nice work on the tests!

@haeseoklee
Copy link
Contributor Author

@Hixie
I found a fatal bug before updating my code.

When ReorderableListView was first built, the order could be changed, but an error occurred after pressing the refresh button.

After a bug occurs, the order cannot be changed and the state is initialized.

I would like to ask your advice.

example code
import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {

  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(title: 'ReorderableListView issue demo',),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  final List<int> data = List.generate(10, (index) => index);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: [_buildRefreshBtn()],
      ),
      body: Center(
          child: ReorderableListView(
            children: _buildTiles(),
            onReorder: (_, __) {},
          )
      ),
    );
  }

  Widget _buildRefreshBtn() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20.0),
      child: IconButton(
          icon: Icon(Icons.refresh),
          onPressed: (){setState(() {});}
      ),
    );
  }

  List<Widget> _buildTiles() {
    List<Widget> tiles = data.map((int val) {

      return MyListTile(
        key: ValueKey(val),
        data: val,
        title: "tile $val",
      );
    }).toList();
    return tiles;
  }
}


class MyListTile extends StatefulWidget {
  final int data;
  final String title;


  MyListTile({this.data, this.title, Key key}): super(key: key);

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


class _MyListTileState extends State<MyListTile> {
  Stream<int> _msStream;
  int _curMs;

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

    print("init: ${widget.title}");
    _curMs = 0;
    _msStream = Stream.periodic(const Duration(milliseconds: 100), (ms) => ms);

  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 30.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text("${widget.title}"),
          StreamBuilder<int>(
              stream: _msStream,
              initialData: _curMs,
              builder: (_, AsyncSnapshot<int> snapshot) {
                if (!snapshot.hasData) return Center(child: CircularProgressIndicator());

                String ms = "${snapshot.data} ms";
                return Text(ms);
              }
          )
        ],
      ),
    );
  }

  void dispose(){
    print("dispose: ${widget.title}");
    super.dispose();
  }
}
log
Launching lib/main.dart on iPhone SE (2nd generation) in debug mode...
Running Xcode build...
Xcode build done.                                            9.4s
Waiting for iPhone SE (2nd generation) to report its views...
Debug service listening on ws://127.0.0.1:50703/IbMyf_8lHco=/ws
Syncing files to device iPhone SE (2nd generation)...
flutter: init: tile 0
flutter: init: tile 1
flutter: init: tile 2
flutter: init: tile 3
flutter: init: tile 4
flutter: init: tile 5
flutter: init: tile 6
flutter: init: tile 7
flutter: init: tile 8
flutter: init: tile 9

════════ Exception caught by gesture ═══════════════════════════════════════════════════════════════
The following NoSuchMethodError was thrown while handling a gesture:
The getter 'size' was called on null.
Receiver: null
Tried calling: size

When the exception was thrown, this was the stack: 
#0      Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5)
#1      _ReorderableListContentState._wrap.onDragStarted.<anonymous closure> (package:flutter/src/material/reorderable_list.dart:379:66)
#2      State.setState (package:flutter/src/widgets/framework.dart:1244:30)
#3      _ReorderableListContentState._wrap.onDragStarted (package:flutter/src/material/reorderable_list.dart:373:7)
#4      _DraggableState._startDrag (package:flutter/src/widgets/drag_target.dart:425:27)
...
Handler: "onStart"
Recognizer: DelayedMultiDragGestureRecognizer#96287
════════════════════════════════════════════════════════════════════════════════════════════════════
flutter: init: tile 0
flutter: dispose: tile 0

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
'package:flutter/src/widgets/framework.dart': Failed assertion: line 175 pos 16: 'element.widget.runtimeType != _registry[this].widget.runtimeType': is not true.
════════════════════════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by gesture ═══════════════════════════════════════════════════════════════
The following NoSuchMethodError was thrown while handling a gesture:
The getter 'size' was called on null.
Receiver: null
Tried calling: size

When the exception was thrown, this was the stack: 
#0      Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5)
#1      _ReorderableListContentState._wrap.onDragStarted.<anonymous closure> (package:flutter/src/material/reorderable_list.dart:379:66)
#2      State.setState (package:flutter/src/widgets/framework.dart:1244:30)
#3      _ReorderableListContentState._wrap.onDragStarted (package:flutter/src/material/reorderable_list.dart:373:7)
#4      _DraggableState._startDrag (package:flutter/src/widgets/drag_target.dart:425:27)
...
Handler: "onStart"
Recognizer: DelayedMultiDragGestureRecognizer#b9436
════════════════════════════════════════════════════════════════════════════════════════════════════
flutter: init: tile 2
flutter: dispose: tile 2

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
'package:flutter/src/widgets/framework.dart': Failed assertion: line 175 pos 16: 'element.widget.runtimeType != _registry[this].widget.runtimeType': is not true.
════════════════════════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
Duplicate GlobalKey detected in widget tree.
════════════════════════════════════════════════════════════════════════════════════════════════════

@Hixie
Copy link
Contributor

Hixie commented Aug 31, 2020

After a bug occurs, the order cannot be changed and the state is initialized.

If the exception occurs during build then you're probably ending up with this widget in an inconsistent state. The solution is to make the code not throw the first exception (it wasn't clear to me if the first exception you're seeing is from the framework/this widget or if it's from user code).

@haeseoklee
Copy link
Contributor Author

@Hixie
I can't come up with a clear solution, but I'll try again. Thank you very much :)

@haeseoklee
Copy link
Contributor Author

@Hixie
As a result I solved this problem. I guess the example code was wrong.

In the example, each time setState() is called, ValueKey(index) is reassigned.

Here's how I did it.

final List<Key> keyValues = List.generate(10, (index) => ValueKey(index));
List<Widget> _buildTiles() {
    List<Widget> tiles = data.map((int val) {

      return MyListTile(
        key: keyValues[val],
        data: val,
        title: "tile $val",
      );
    }).toList();
    return tiles;
  }

No other problems occurred after I tested it.

I would be grateful if you could review my PR again.

/// The difference with GlobalObjectKey is that it uses [==] instead of [identical]
/// of the objects used to generate widgets.
@optionalTypeArgs
class _ReorderableListViewChildGlobalKey<T extends State<StatefulWidget>> extends GlobalObjectKey<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

you don't need to make this generic, since you don't use the T in the implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! I updated the code.

@Hixie
Copy link
Contributor

Hixie commented Sep 4, 2020

Patch looks good, we should get a second reviewer just to double check, but otherwise I think it's good. Which example was it you said had problems?

@haeseoklee
Copy link
Contributor Author

Here is the example code.

I agree. It's a good idea to find a second reviewer and check the code.

example code
import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {

  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(title: 'ReorderableListView issue demo',),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  final List<int> data = List.generate(10, (index) => index);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: [_buildRefreshBtn()],
      ),
      body: Center(
          child: ReorderableListView(
            children: _buildTiles(),
            onReorder: (_, __) {},
          )
      ),
    );
  }

  Widget _buildRefreshBtn() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20.0),
      child: IconButton(
          icon: Icon(Icons.refresh),
          onPressed: (){setState(() {});}
      ),
    );
  }

  List<Widget> _buildTiles() {
    List<Widget> tiles = data.map((int val) {

      return MyListTile(
        key: ValueKey(val),
        data: val,
        title: "tile $val",
      );
    }).toList();
    return tiles;
  }
}


class MyListTile extends StatefulWidget {
  final int data;
  final String title;


  MyListTile({this.data, this.title, Key key}): super(key: key);

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


class _MyListTileState extends State<MyListTile> {
  Stream<int> _msStream;
  int _curMs;

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

    print("init: ${widget.title}");
    _curMs = 0;
    _msStream = Stream.periodic(const Duration(milliseconds: 100), (ms) => ms);

  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 30.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text("${widget.title}"),
          StreamBuilder<int>(
              stream: _msStream,
              initialData: _curMs,
              builder: (_, AsyncSnapshot<int> snapshot) {
                if (!snapshot.hasData) return Center(child: CircularProgressIndicator());

                String ms = "${snapshot.data} ms";
                return Text(ms);
              }
          )
        ],
      ),
    );
  }

  void dispose(){
    print("dispose: ${widget.title}");
    super.dispose();
  }
}

@haeseoklee
Copy link
Contributor Author

@chunhtai
I would appreciate it if you could review the code.
It seems to be related to other PRs.
#41338
#41931


@override
int get hashCode => super.hashCode;
}
Copy link
Member

@xu-baolin xu-baolin Sep 7, 2020

Choose a reason for hiding this comment

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

@Hixie Add _ReorderableListContentState as a value of the key can not solve the situation where multiple items use the same local key, they have the same _ReorderableListContentState, which will lead to Multiple widgets used the same GlobalKey, such as:

  @override
  Widget build(BuildContext context) {
    return ReorderableListView(
      children: <Widget>[ InnerTest(key: ValueKey(0)), InnerTest(key: ValueKey(0)) ],
      onReorder: (_, __) {},
    );
  }

The reason why there is no problem here is its wrong implementation of int get hashCode => identityHashCode(subKey);

Probably we could add the index into the values together to solve this problem.

I think we should add this to the unit test case.

}

@override
int get hashCode => identityHashCode(subKey);
Copy link
Member

@xu-baolin xu-baolin Sep 7, 2020

Choose a reason for hiding this comment

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

    If [operator ==] is overridden to use the object state instead,
    the hash code must also be changed to represent that state.

In other words, Hash codes must be the same for objects that are equal to each other according to [operator ==]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about this way?

@override
int get hashCode => hashValues(subKey.hashCode, identityHashCode(state));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new hash seems to work fine :)

Copy link
Member

Choose a reason for hiding this comment

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

@override
int get hashCode => hashValues(subKey, state, index);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we need an index? ValueKey already has a value for the index.

Copy link
Member

Choose a reason for hiding this comment

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

/// particular type to identify itself.
///
/// The difference with GlobalObjectKey is that it uses [==] instead of [identical]
/// of the objects used to generate widgets.
Copy link
Member

Choose a reason for hiding this comment

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

Comments whit // .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know much about the hash in the key. Would you explain with an example?

Copy link
Member

Choose a reason for hiding this comment

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

Also see

/**
   * The equality operator.
   *
   * The default behavior for all [Object]s is to return true if and
   * only if `this` and [other] are the same object.
   *
   * Override this method to specify a different equality relation on
   * a class. The overriding method must still be an equivalence relation.
   * That is, it must be:
   *
   *  * Total: It must return a boolean for all arguments. It should never throw
   *    or return `null`.
   *
   *  * Reflexive: For all objects `o`, `o == o` must be true.
   *
   *  * Symmetric: For all objects `o1` and `o2`, `o1 == o2` and `o2 == o1` must
   *    either both be true, or both be false.
   *
   *  * Transitive: For all objects `o1`, `o2`, and `o3`, if `o1 == o2` and
   *    `o2 == o3` are true, then `o1 == o3` must be true.
   *
   * The method should also be consistent over time,
   * so whether two objects are equal should only change
   * if at least one of the objects was modified.
   *
   * If a subclass overrides the equality operator it should override
   * the [hashCode] method as well to maintain consistency.
   */
  external bool operator ==(Object other);

  /**
   * The hash code for this object.
   *
   * A hash code is a single integer which represents the state of the object
   * that affects [operator ==] comparisons.
   *
   * All objects have hash codes.
   * The default hash code represents only the identity of the object,
   * the same way as the default [operator ==] implementation only considers objects
   * equal if they are identical (see [identityHashCode]).
   *
   * If [operator ==] is overridden to use the object state instead,
   * the hash code must also be changed to represent that state.
   *
   * Hash codes must be the same for objects that are equal to each other
   * according to [operator ==].
   * The hash code of an object should only change if the object changes
   * in a way that affects equality.
   * There are no further requirements for the hash codes.
   * They need not be consistent between executions of the same program
   * and there are no distribution guarantees.
   *
   * Objects that are not equal are allowed to have the same hash code,
   * it is even technically allowed that all instances have the same hash code,
   * but if clashes happen too often, it may reduce the efficiency of hash-based
   * data structures like [HashSet] or [HashMap].
   *
   * If a subclass overrides [hashCode], it should override the
   * [operator ==] operator as well to maintain consistency.
   */
  external int get hashCode;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you very much 👍

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@xu-baolin @PixelToast
It looks better to modify it with //. Thank you for your guidance.

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

Change looks reasonable. Added a comment about test

expect(findState(const Key('A')).checked, true);
});

testWidgets('Preserves children states when rebuilt', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the same test as previous one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. One is for''vertical mode" and the other is for "horizontal mode"

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, but then you will need to set scrollDirection to vertical in this test, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right. I'll apply it right away. 👍

@haeseoklee
Copy link
Contributor Author

@chunhtai
I added comments about test.

@chunhtai
Copy link
Contributor

chunhtai commented Sep 8, 2020

@chunhtai
I added comments about test.

I think we need to set the scrollDirection to the test, I also replied to the comment

@haeseoklee
Copy link
Contributor Author

@chunhtai
Thank you for your comment. I applied it.

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

LGTM

@awhitford
Copy link

I'm looking forward to this. Which Flutter version will contain this fix?

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 12, 2021
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.

ReorderableListView destroys children even if Key-Type of children wasn't changed

8 participants