【问题标题】:Deleting item from Flutters ListView removes the last widget only从 Flutters ListView 中删除项目仅删除最后一个小部件
【发布时间】:2020-04-09 18:49:41
【问题描述】:

我有一个包含StateFull WidgetsListView,每个Tiles 代表列表中的一个对象。问题是当我尝试从该列表中删除对象时,屏幕上只会删除最后一个Tile。对于删除项目,我使用对每个 Tile 的适当方法 (_deleteItem(_HomeItem item)) 的引用。我怀疑这是我使用的Keys 的问题,但我不确定。我已经尝试使用不同的键(即ObjectKey(item)GlobalKey<_TileState>()),但这并没有改变任何东西。

我只发现了一个关于我的问题here 的问题。但是那里的解决方案要么不起作用,要么我错误地遵循了它们。

这是我尝试做的最小工作示例:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Slidable Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Slidable Demo'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final List<_HomeItem> items = List.generate(
    5,
    (i) => _HomeItem(
      i,
      'Tile n°$i',
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: _buildList(context),
      ),
    );
  }

  Widget _buildList(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) {
        return Tile(items[index], _deleteItem);
      },
      itemCount: items.length,
    );
  }

  void _deleteItem(_HomeItem item) {
    setState(() {
      print(context);
      print("remove: $item");
      print("Number of items before: ${items.length}");
      items.remove(item);
      print("Number of items after delete: ${items.length}");
    });
  }
}

class Tile extends StatefulWidget {
  final _HomeItem item;
  final Function delete;

  Tile(this.item, this.delete);

  @override
  State<StatefulWidget> createState() => _TileState(item, delete);
}

class _TileState extends State<Tile> {
  final _HomeItem item;
  final Function delete;

  _TileState(this.item, this.delete);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      key: ValueKey(item.index),
      title: Text("${item.title}"),
      subtitle: Text("${item.index}"),
      onTap: () => delete(item),
    );
  }
}

class _HomeItem {
  const _HomeItem(
    this.index,
    this.title,
  );

  final int index;
  final String title;
} 

因为这会导致误解,所以现在列表中的每个项目是这样的:

/*
  privacyIDEA Authenticator

  Authors: Timo Sturm <timo.sturm@netknights.it>

  Copyright (c) 2017-2019 NetKnights GmbH

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:privacyidea_authenticator/model/tokens.dart';
import 'package:privacyidea_authenticator/utils/storageUtils.dart';
import 'package:privacyidea_authenticator/utils/util.dart';

class TokenWidget extends StatefulWidget {
  final Token _token;
  final Function _delete;

  TokenWidget(this._token, this._delete);

  @override
  State<StatefulWidget> createState() {
    if (_token is HOTPToken) {
      return _HotpWidgetState(_token, _delete);
    } else if (_token is TOTPToken) {
      return _TotpWidgetState(_token, _delete);
    } else {
      throw ArgumentError.value(_token, "token",
          "The token [$_token] is of unknown type and not supported.");
    }
  }
}

abstract class _TokenWidgetState extends State<TokenWidget> {
  final Token _token;
  static final SlidableController _slidableController = SlidableController();
  String _otpValue;
  String _label;

  final Function _delete;

  _TokenWidgetState(this._token, this._delete) {
    _otpValue = calculateOtpValue(_token);
    _saveThisToken();
    _label = _token.label;
  }

  @override
  Widget build(BuildContext context) {
    return Slidable(
      key: ValueKey(_token.serial),
      // This is used to only let one Slidable be open at a time.
      controller: _slidableController,
      actionPane: SlidableDrawerActionPane(),
      actionExtentRatio: 0.25,
      child: Container(
        color: Colors.white,
        child: _buildTile(),
      ),
      secondaryActions: <Widget>[
        IconSlideAction(
          caption: 'Delete',
          color: Colors.red,
          icon: Icons.delete,
          onTap: () => _deleteTokenDialog(),
        ),
        IconSlideAction(
          caption: 'Rename',
          color: Colors.blue,
          icon: Icons.edit,
          onTap: () => _renameTokenDialog(),
        ),
      ],
    );
  }

  // TODO Test this behaviour with integration testing.
  void _renameTokenDialog() {
    final _nameInputKey = GlobalKey<FormFieldState>();
    String _selectedName;

    showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text("Rename token"),
            titleTextStyle: Theme.of(context).textTheme.subhead,
            content: TextFormField(
              autofocus: true,
              initialValue: _label,
              key: _nameInputKey,
              onChanged: (value) => this.setState(() => _selectedName = value),
              decoration: InputDecoration(labelText: "Name"),
              validator: (value) {
                if (value.isEmpty) {
                  return 'Please enter a name for this token.';
                }
                return null;
              },
            ),
            actions: <Widget>[
              FlatButton(
                child: Text("Rename"),
                onPressed: () {
                  if (_nameInputKey.currentState.validate()) {
                    _renameToken(_selectedName);
                    Navigator.of(context).pop();
                  }
                },
              ),
              FlatButton(
                child: Text("Cancel"),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ],
          );
        });
  }

  void _renameToken(String newLabel) {
    _saveThisToken();
    log(
      "Renamed token:",
      name: "token_widgets.dart",
      error: "\"${_token.label}\" changed to \"$newLabel\"",
    );
    _token.label = newLabel;

    setState(() {
      _label = _token.label;
    });
  }

  void _deleteTokenDialog() {
    showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text("Confirm deletion"),
            titleTextStyle: Theme.of(context).textTheme.subhead,
            content: RichText(
              text: TextSpan(
                  style: TextStyle(
                    color: Colors.black,
                  ),
                  children: [
                    TextSpan(
                      text: "Are you sure you want to delete ",
                    ),
                    TextSpan(
                        text: "\'$_label\'?",
                        style: TextStyle(
                          fontStyle: FontStyle.italic,
                        ))
                  ]),
            ),
            actions: <Widget>[
              FlatButton(
                onPressed: () => {
                  _delete(_token),
                  Navigator.of(context).pop(),
                },
                child: Text("Yes!"),
              ),
              FlatButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text("No, take me back!"),
              ),
            ],
          );
        });
  }

  void _saveThisToken() {
    StorageUtil.saveOrReplaceToken(this._token);
  }

  void _updateOtpValue();

  Widget _buildTile();
}

class _HotpWidgetState extends _TokenWidgetState {
  _HotpWidgetState(Token token, Function delete) : super(token, delete);

  @override
  void _updateOtpValue() {
    setState(() {
      (_token as HOTPToken).incrementCounter();
      _otpValue = calculateOtpValue(_token);
    });
  }

  @override
  Widget _buildTile() {
    return Stack(
      children: <Widget>[
        ListTile(
          title: Center(
            child: Text(
              insertCharAt(_otpValue, " ", _otpValue.length ~/ 2),
              textScaleFactor: 2.5,
            ),
          ),
          subtitle: Center(
            child: Text(
              _label,
              textScaleFactor: 2.0,
            ),
          ),
        ),
        Align(
          alignment: Alignment.centerRight,
          child: RaisedButton(
            onPressed: () => _updateOtpValue(),
            child: Text(
              "Next",
              textScaleFactor: 1.5,
            ),
          ),
        ),
      ],
    );
  }
}

class _TotpWidgetState extends _TokenWidgetState
    with SingleTickerProviderStateMixin {
  AnimationController
      controller; // Controller for animating the LinearProgressAnimator

  _TotpWidgetState(Token token, Function delete) : super(token, delete);

  @override
  void _updateOtpValue() {
    setState(() {
      _otpValue = calculateOtpValue(_token);
    });
  }

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

    controller = AnimationController(
      duration: Duration(seconds: (_token as TOTPToken).period),
      // Animate the progress for the duration of the tokens period.
      vsync:
          this, // By extending SingleTickerProviderStateMixin we can use this object as vsync, this prevents offscreen animations.
    )
      ..addListener(() {
        // Adding a listener to update the view for the animation steps.
        setState(() => {
              // The state that has changed here is the animation object’s value.
            });
      })
      ..addStatusListener((status) {
        // Add listener to restart the animation after the period, also updates the otp value.
        if (status == AnimationStatus.completed) {
          controller.forward(from: 0.0);
          _updateOtpValue();
        }
      })
      ..forward(); // Start the animation.

    // Update the otp value when the android app resumes, this prevents outdated otp values
    // ignore: missing_return
    SystemChannels.lifecycle.setMessageHandler((msg) {
      log(
        "SystemChannels:",
        name: "totpwidget.dart",
        error: msg,
      );
      if (msg == AppLifecycleState.resumed.toString()) {
        _updateOtpValue();
      }
    });
  }

  @override
  void dispose() {
    controller.dispose(); // Dispose the controller to prevent memory leak.
    super.dispose();
  }

  @override
  Widget _buildTile() {
    return Column(
      children: <Widget>[
        ListTile(
          title: Center(
            child: Text(
              insertCharAt(_otpValue, " ", _otpValue.length ~/ 2),
              textScaleFactor: 2.5,
            ),
          ),
          subtitle: Center(
            child: Text(
              _label,
              textScaleFactor: 2.0,
            ),
          ),
        ),
        LinearProgressIndicator(
          value: controller.value,
        ),
      ],
    );
  }
}

计划是在未来将不同类型的项目添加到列表中,每个项目可能会呈现完全不同的内容,并且需要不同的逻辑。完整项目请参考github。虽然我确信这是为列表创建不同小部件的正确方法,但我愿意接受不同的方法来做到这一点。

【问题讨论】:

    标签: listview flutter


    【解决方案1】:

    试试这个:

    
    
    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Slidable Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Slidable Demo'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
    
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      final List<_HomeItem> items = List.generate(
        5,
        (i) => _HomeItem(
          i,
          'Tile n°$i',
        ),
      );
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: _buildList(context),
          ),
        );
      }
    
      Widget _buildList(BuildContext context) {
        return ListView.builder(
          itemBuilder: (BuildContext context, int i) {
            _HomeItem item = items[i];
              return ListTile(
    
                title: Text(item.index.toString()),
                subtitle: Text(item.title.toString()),
                onTap: () {
                  setState(() {
                    items.removeAt(i);
                  });
                },
    
              );
          },
          itemCount: items.length,
        );
      }
    }
    
    
    
    class _HomeItem {
      const _HomeItem(
        this.index,
        this.title,
      );
    
      final int index;
      final String title;
    } 
    
    

    其他解决方案:-

    
    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Slidable Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Slidable Demo'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
       List<_HomeItem> items = [];
    
       @override
      void initState() {
        for(int i=0; i<5; i++) {
          items.add(_HomeItem(i, 'Title $i', Key(i.toString())));
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: _buildList(context),
          ),
        );
      }
    
      Widget _buildList(BuildContext context) {
        return ListView.builder(
          itemBuilder: (context, index) {
            return tiles(items[index], _deleteItem);
          },
          itemCount: items.length,
        );
      }
    
      Widget tiles(_HomeItem item, Function detete) {
        return ListTile(
                key: item.key,
                title: Text("${item.title}"),
                subtitle: Text("${item.index}"),
                onTap: () => detete(item),
          );
      } 
        void _deleteItem(_HomeItem itemtoDelete,) {
        for(int i =0; i <items.length; i++) {
            if(items[i].key == itemtoDelete.key) {
              setState(() {
                items.removeWhere((item) => item.key == itemtoDelete.key);
              });
            }else {
            }
        }
        }
      }
    
    class _HomeItem {
      const _HomeItem(
        this.index,
        this.title,
        this.key,
      );
    
      final int index;
      final String title;
       final Key key;
    
    } 
    
    

    【讨论】:

    • 感谢您的建议。这确实是一个可能的解决方案。但这会迫使我将所有逻辑转移到_MyHomePageState 类。由于该列表必须显示至少三个不同的 _HomeItem 子类,每个子类都有不同的逻辑和视觉表示,如果我能阻止它,这不是我想要的方式。
    • 解释一下为什么这是一个解决方案会很好。 @OnkelZukunft,如果您在更改列表时使用setState,您的列表只会由 Flutter 重建。您不必将所有逻辑移到那里,但您必须调用 setState 并在那里设置一个新列表。
    • 我已经更新了我的答案添加了一个替代解决方案。
    • 您的 Tile() 类只是渲染 UI,那么您希望在那里运行什么业务逻辑?其他可能的解决方案可以是您可以为每个 Tile 小部件定义唯一的 Key 并比较它们以删除。我认为您不需要为此开设单独的课程。
    • 我更新了我的问题,我希望这能让我更清楚我试图完成什么以及如何完成。 @Iyio 据我了解,我确实打电话给setState(),但我不知道为什么 ListView 不会更新。
    【解决方案2】:

    您应该为每个 Tile 设置Key。您可以观看此Flutter Tutorial 以了解您的代码中发生了什么。

    我在下面的代码中使用了ObjectKey

    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Slidable Demo',
          home: MyListPage(),
        );
      }
    }
    
    class MyListPage extends StatefulWidget {
      MyListPage({Key key}) : super(key: key);
    
      @override
      _MyListPageState createState() => _MyListPageState();
    }
    
    class _MyListPageState extends State<MyListPage> {
      final List<Item> items = [
        Item("ItemName_1", "ItemType_1"),
        Item("ItemName_2", "ItemType_2"),
        Item("ItemName_3", "ItemType_1"),
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Flutter Slidable Demo"),
          ),
          body: _buildList(context),
        );
      }
    
      Widget _buildList(BuildContext context) {
        return ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            final item = items[index];
            return MyTile(
              key: ObjectKey(item),
              item: items[index],
              onDeleteClicked: () => _deleteItem(item),
            );
          },
        );
      }
    
      void _deleteItem(Item item) {
        items.remove(item);
        setState(() {});
      }
    }
    
    class Item {
      String itemName;
      String itemType;
    
      Item(this.itemName, this.itemType);
    }
    
    class MyTile extends StatefulWidget {
      final Item item;
      final VoidCallback onDeleteClicked;
    
      const MyTile({Key key, this.item, this.onDeleteClicked}) : super(key: key);
    
      @override
      _MyTileState createState() {
        if (item.itemType == "ItemType_1")
          return TileStateType1();
        else if (item.itemType == "ItemType_2")
          return TileStateType2();
        else
          throw ArgumentError.value("Unknown Item type and not supported.");
      }
    }
    
    abstract class _MyTileState extends State<MyTile> {
      void _renameItem() {
        final txtCtrl = TextEditingController(text: widget.item.itemName);
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text("Rename"),
              content: TextField(
                controller: txtCtrl,
              ),
              actions: <Widget>[
                RaisedButton(
                  color: Colors.green,
                  child: Text("Confirm", style: TextStyle(color: Colors.white)),
                  onPressed: () {
                    setState(() {
                      widget.item.itemName = txtCtrl.text;
                    });
                    Navigator.pop(context);
                  },
                ),
                RaisedButton(
                  color: Colors.red,
                  child: Text("Cancel", style: TextStyle(color: Colors.white)),
                  onPressed: () => Navigator.pop(context),
                ),
              ],
            );
          },
        );
      }
    
      void _deleteItem() {
        showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text("Delete"),
                content: Text("Are you sure?"),
                actions: <Widget>[
                  RaisedButton(
                    color: Colors.green,
                    child: Text("Confirm", style: TextStyle(color: Colors.white)),
                    onPressed: () {
                      widget.onDeleteClicked();
                      Navigator.pop(context);
                    },
                  ),
                  RaisedButton(
                    color: Colors.red,
                    child: Text("Cancel", style: TextStyle(color: Colors.white)),
                    onPressed: () => Navigator.pop(context),
                  ),
                ],
              );
            });
      }
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Material(
            elevation: 3.0,
            color: Colors.white,
            child: Row(
              children: <Widget>[
                Expanded(
                  child: InkWell(
                    onTap: _doDifferently,
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: _buildDifferently(),
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.edit),
                  onPressed: _renameItem,
                ),
                IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: _deleteItem,
                ),
              ],
            ),
          ),
        );
      }
    
      Widget _buildDifferently();
    
      void _doDifferently();
    }
    
    class TileStateType1 extends _MyTileState {
      @override
      Widget _buildDifferently() {
        return Row(
          children: <Widget>[
            Text(
              widget.item.itemName,
              style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
            ),
            Material(
              color: Colors.blue,
              child: Padding(
                padding: const EdgeInsets.all(5.0),
                child: Text(
                  widget.item.itemType,
                  style: TextStyle(fontSize: 12, color: Colors.white),
                ),
              ),
            )
          ],
        );
      }
    
      @override
      void _doDifferently() {
        Scaffold.of(context).showSnackBar(SnackBar(
          content: Text("I am type 1"),
        ));
      }
    }
    
    class TileStateType2 extends _MyTileState {
      @override
      Widget _buildDifferently() {
        return Row(
          children: <Widget>[
            Icon(Icons.security),
            Text(
              widget.item.itemName,
              style: TextStyle(color: Colors.green, fontStyle: FontStyle.italic),
            ),
            Material(
              color: Colors.red,
              shape: StadiumBorder(),
              child: Padding(
                padding: const EdgeInsets.all(5.0),
                child: Text(
                  widget.item.itemType,
                  style: TextStyle(fontSize: 12, color: Colors.white),
                ),
              ),
            ),
          ],
        );
      }
    
      @override
      void _doDifferently() {
        showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text("Message"),
                content: Text("I am type 2"),
              );
            });
      }
    }
    

    【讨论】:

    • 非常感谢,解决方案确实是为列表中的每个 Tile 提供一个Key
    猜你喜欢
    • 2021-08-24
    • 1970-01-01
    • 1970-01-01
    • 2020-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多