Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deleting item from Flutters ListView removes the last widget only

I have a ListView that contains StateFull Widgets, each of these Tiles represents one object from a list. The problem is that when I try to delete objects from that list, only the last Tile will be removed on the screen. For deleting items I use a reference to the appropriate method (_deleteItem(_HomeItem item)) to each Tile. I suspect this is a problem with the Keys I use, but I am not sure. I already tried to use different keys (i.e. ObjectKey(item) and GlobalKey<_TileState>()) instead, but that did not change anything.

I found only one question regarding my problem here. But the solutions there either do not work, or I followed them incorrectly.

This is a minimal working example for what I try to do:

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;
} 

Because this lead to misunderstandings, here is what each item in the list is right now:

/*
  privacyIDEA Authenticator

  Authors: Timo Sturm <[email protected]>

  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,
        ),
      ],
    );
  }
}

The plan is to add different kind of items to the list in the future, each of that will probably be presented completely different, and also require different logic. For the full project please refer to github. While I am convinced that this is the right way to create different Widgets for the list, I am open for different approaches to do this.

like image 390
OnkelZukunft Avatar asked Dec 02 '22 09:12

OnkelZukunft


1 Answers

You should set Key for each Tile. You can watch this Flutter Tutorial to understand what is going on in your code.

I used ObjectKey in the following code.

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"),
          );
        });
  }
}
like image 102
Crazy Lazy Cat Avatar answered Dec 04 '22 03:12

Crazy Lazy Cat