flutter_slidable icon indicating copy to clipboard operation
flutter_slidable copied to clipboard

Unexpected behavior when deleting Slidable from a list

Open MobiliteDev opened this issue 4 years ago • 5 comments

I'm currently using flutter_slidable version 1.0.0-dev.8. From your example, I have built a dynamic list of Sidable that can be deleted. I'm not sure this is the expected behavior or if I need to do some more coding but when I delete one of the Slidable, all the other Slidable under it lost their state :

All the Slidable are dragged : image

Then I delete one of them (third one in this case) : image

The two first item kept their state and are still dragged but the two last aren't.

This is the code for the widget :

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

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

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  List<Item> listeItems = <Item>[];
  int cpt = 5;

  @override
  void initState() {
    super.initState();
    listeItems.add(Item(rang: 1));
    listeItems.add(Item(rang: 2));
    listeItems.add(Item(rang: 3));
    listeItems.add(Item(rang: 4));
    controller = AnimationController(
      vsync: this,
      upperBound: 0.5,
      duration: const Duration(milliseconds: 2000),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Slidable'),
      ),
      body: SlidablePlayer(
        animation: controller,
        child: ListView.builder(
          itemBuilder: (context, index) {
            final Item itemEnCours = listeItems[index];
            return MySlidable(
              key: ObjectKey(itemEnCours),
              motion: const BehindMotion(),
              rang: itemEnCours.rang,
              onTap: (BuildContext context) {
                listeItems.remove(itemEnCours);
                setState(() {});
              },
            );
          },
          itemCount: listeItems.length,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          listeItems.add(Item(rang: cpt++));
          setState(() {});
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

MySlidable Widget :

class MySlidable extends StatelessWidget {
  const MySlidable(
      {Key key, @required this.motion, @required this.rang, this.onTap})
      : super(key: key);

  final Widget motion;

  final void Function(BuildContext) onTap;

  final int rang;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Slidable(
        startActionPane: ActionPane(
          motion: motion,
          children: <Widget>[
            SlidableAction(
              onPressed: onTap,
              backgroundColor: Color(0xFFFE4A49),
              icon: Icons.delete,
              label: 'Delete',
            ),
          ],
        ),
        child: SlidableControllerSender(
          child: Tile(text: motion.runtimeType.toString() + rang.toString()),
        ),
      ),
    );
  }
}

Item class :

class Item {
  int rang;

  Item({@required this.rang});
}

Thank you for your time.

MobiliteDev avatar Jul 29 '21 09:07 MobiliteDev

You need to have a different keys for each Slidable otherwise you will have strange results. If your rang property is unique, you could use it with a ValueKey for example.

letsar avatar Jul 29 '21 19:07 letsar

I tried it but it's not working either eventhough rang property is unique : image

The result is the same as before.

MobiliteDev avatar Jul 30 '21 14:07 MobiliteDev

Can you provide a sample code, I could run so that I can see what's going on?

letsar avatar Jul 31 '21 16:07 letsar

Here is my full code

import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';

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

const actions = [
  SlideAction(
    color: Color(0xFFFE4A49),
    icon: Icons.delete,
    label: 'Delete',
  ),
  SlideAction(
    color: Color(0xFF21B7CA),
    icon: Icons.share,
    label: 'Share',
  ),
];

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Slidable',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  List<Item> listeItems = <Item>[];
  int cpt = 5;

  SlidableController controllerSlide;

  @override
  void initState() {
    super.initState();
    listeItems.add(Item(rang: 1));
    listeItems.add(Item(rang: 2));
    listeItems.add(Item(rang: 3));
    listeItems.add(Item(rang: 4));
    controller = AnimationController(
      vsync: this,
      upperBound: 0.5,
      duration: const Duration(milliseconds: 2000),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Slidable'),
      ),
      body: SlidablePlayer(
        animation: controller,
        child: ListView.builder(
          itemBuilder: (context, index) {
            final Item itemEnCours = listeItems[index];
            return MySlidable(
              motion: const BehindMotion(),
              rang: itemEnCours.rang,
              onTap: (BuildContext context) {
                listeItems.remove(itemEnCours);
                setState(() {});
              },
            );
          },
          itemCount: listeItems.length,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          listeItems.add(Item(rang: cpt++));
          setState(() {});
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

class SlidablePlayer extends StatefulWidget {
  const SlidablePlayer({
    Key key,
    @required this.animation,
    @required this.child,
  }) : super(key: key);

  final Animation<double> animation;
  final Widget child;

  @override
  _SlidablePlayerState createState() => _SlidablePlayerState();

  static _SlidablePlayerState of(BuildContext context) {
    return context.findAncestorStateOfType<_SlidablePlayerState>();
  }
}

class _SlidablePlayerState extends State<SlidablePlayer> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

class MySlidable extends StatefulWidget {
  const MySlidable(
      {Key key, @required this.motion, @required this.rang, this.onTap})
      : super(key: key);

  final Widget motion;

  final void Function(BuildContext) onTap;

  final int rang;

  @override
  MySlidableSate createState() {
    return MySlidableSate();
  }
}

class MySlidableSate extends State<MySlidable> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Slidable(
        key: ValueKey(widget.rang),
        startActionPane: ActionPane(
          motion: widget.motion,
          children: <Widget>[
            SlidableAction(
              onPressed: widget.onTap,
              backgroundColor: Color(0xFFFE4A49),
              icon: Icons.delete,
              label: 'Delete',
            ),
          ],
        ),
        child: Tile(
            text:
                widget.motion.runtimeType.toString() + widget.rang.toString()),
      ),
    );
  }
}

class Tile extends StatelessWidget {
  const Tile({
    Key key,
    this.color = const Color(0xFFF4F4F8),
    @required this.text,
  }) : super(key: key);

  final Color color;
  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      height: 100,
      child: Center(child: Text(text)),
    );
  }
}

class Item {
  int rang;

  Item({@required this.rang});
}

class SlideAction extends StatelessWidget {
  const SlideAction({
    Key key,
    @required this.color,
    @required this.icon,
    @required this.label,
    this.onTap,
    this.flex = 1,
  }) : super(key: key);

  final Color color;
  final IconData icon;
  final int flex;
  final String label;
  final void Function(BuildContext) onTap;

  @override
  Widget build(BuildContext context) {
    return SlidableAction(
      flex: flex,
      backgroundColor: color,
      foregroundColor: Colors.white,
      onPressed: onTap,
      icon: icon,
      label: label,
    );
  }
}


MobiliteDev avatar Aug 09 '21 08:08 MobiliteDev

Oh ok, this is not an issue of the package but you need to set the Key to key: ValueKey(itemEnCours), and also set the findChildIndexCallback of the ListView as is:

findChildIndexCallback: (key) {
  if (key is ValueKey<Item>) {
    return listeItems.indexOf(key.value);
  }
},

This is explained in https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html:

The findChildIndexCallback corresponds to the SliverChildBuilderDelegate.findChildIndexCallback property. If null, a child widget may not map to its existing RenderObject when the order of children returned from the children builder changes. This may result in state-loss. This callback needs to be implemented if the order of the children may change at a later time.

letsar avatar Jul 10 '22 13:07 letsar