4

So I have been trying to solve this issue for a couple of days now and I seem to have hit a real dead end here.

The issue (which I have simplified in the code below) is that when I try to remove an item from a List<"SomeDataObject">, it does actually remove the correct object from the list. This is evident because the ID that I arbitrarily assigned to the objects does shift just as I would expect it to when I remove something. However, oddly enough, even though the IDs shift in the list and the correct ID is removed, the states of all the widgets seem to act as though the last item is always removed, even when the deleted object is at the middle or even beginning of the list.

An example would be if the list looked as such:

Data(id: 630) // This one starts blue
Data(id: 243) // Let's say the user made this one red
Data(id: 944) // Also blue

And let's say I tried to remove the middle item from this list. Well what happens is that the list will look like this now:

Data(id: 630)
Data(id: 944)

This seems at first to be great because it looks like exactly what I wanted, but for some reason, the colors did not change their order and are still Red first, then Blue. The color state data seems to function independently from the actual objects and I could not find a clear solution.

I have code of an example program to reproduce this problem and I also have some pictures below of the program so that it is clearer what the issue is.

import 'dart:math';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: HomeScreen(),
    );
  }
}

// Home screen class to display app
class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<DataWidget> widgetsList = List<DataWidget>();

  // Adds a new data object to the widgets list
  void addData() {
    setState(() {
      widgetsList.add(DataWidget(removeData));
    });
  }

  // Removes a given widget from the widgets list
  void removeData(DataWidget toRemove) {
    setState(() {
      widgetsList = List.from(widgetsList)..remove(toRemove);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      /* I used a SingleChildScrollView instead of ListView because ListView
      resets an objects state when it gets out of view so I wrapped the whole
      list in a column and then passed that to the SingleChildScrollView to
      force it to stay in memory. It is worth noting that this problem still
      exists even when using a regular ListView. */
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          // Added a SizedBox just to make column take up whole screen width
          children: [...widgetsList, SizedBox(width: double.infinity)],
        ),
      ),
      // FloatingActionButton just to run addData function for example
      floatingActionButton: FloatingActionButton(onPressed: addData),
    );
  }
}

// Simple class that is a red square that when clicked turns blue when pressed
class DataWidget extends StatefulWidget {
  DataWidget(this.onDoubleTap);
  final Function(DataWidget) onDoubleTap;

  // Just a random number to keep track of which widget this is
  final String id = Random.secure().nextInt(1000).toString();

  @override
  String toStringShort() {
    return id;
  }

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

class _DataWidgetState extends State<DataWidget> {
  /* Here the example state information is just a color, but in the full version
  of this problem this actually has multiple fields */
  Color color = Colors.red;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: GestureDetector(
        // onDoubleTap this passes itself to the delete method of the parent
        onDoubleTap: () => widget.onDoubleTap(widget),
        // Changes color on tap to whatever color it isn't
        onTap: () {
          setState(() {
            color = color == Colors.red ? Colors.blue : Colors.red;
          });
        },
        child: Container(
          child: Center(child: Text(widget.id)),
          width: 200,
          height: 200,
          color: color,
        ),
      ),
    );
  }
}

Before user changes any colors (object ID is displayed as text on container): 1

User changes middle container to blue by tapping: 2

User attempts to delete middle (blue) container by double tapping: enter image description here

As you can see from the image above, the ID was deleted and the ID from below was moved up on the screen, but the color information from the state did not get deleted.

Any ideas for things to try would be greatly appreciated.

OMi Shah
  • 5,768
  • 3
  • 25
  • 34
BryceTheGrand
  • 643
  • 3
  • 9
  • 1
    *"So I have been trying to solve this issue for a couple days now and I seem to have hit a real dead end here."* - you have to use a list of data models (and remove data from that list), not list of widgtes, more here: https://flutter.dev/docs/cookbook/gestures/dismissible – pskink Aug 28 '20 at 04:27
  • That is really close to what I want to do @pskink. The only issue is that these Widgets in my actual app are variable in the amount of data, as they contain their own buttons to add new fields to the individual pieces. Do you think this would be possible to implement in this way or would I have to find another way to get the information from the user? – BryceTheGrand Aug 28 '20 at 04:33
  • 1
    the items of the list can have some buttons, whats the problem with it? it the code i linked `Dismissible` is used but you can add some "delete button" to every list item too – pskink Aug 28 '20 at 04:35
  • Sorry I didn't phrase that well. The list of what I currently have doesn't have a delete button it has an 'add new field' button that adds a new text field within the widget itself, which is then inside the Column in SingleChildScrollView. The issue with the ListView example from that link is that ListViews reset their widgets when out of sight, but the fact that the data is now separate from the widget may actually inadvertently fix that issue too. I think I can make this work with this method but I will have to try in the morning when I continue working on this problem. – BryceTheGrand Aug 28 '20 at 04:49
  • 2
    so when you click that button on item #3 for example, you could add some entry in a `Map` (or something) in your item #3 in data list, then after calling `setState` the `build` method is called and depending on that `Map` it builds zero, one or more text fields – pskink Aug 28 '20 at 04:57
  • I was able to get it to work using the method you send in the first comment. Thanks so much. You just saved me a massive headache! – BryceTheGrand Aug 28 '20 at 15:00
  • sure, your welcome – pskink Aug 28 '20 at 15:03

1 Answers1

3

You need to make a few changes to your DataWidget class to make it work:

// Simple class that is a red square that when clicked turns blue when pressed
class DataWidget extends StatefulWidget {

  Color color = Colors.red; // moved from _DataWidgetState class

  DataWidget(this.onDoubleTap);
  final Function(DataWidget) onDoubleTap;

  // Just a random number to keep track of which widget this is
  final String id = Random.secure().nextInt(1000).toString();

  @override
  String toStringShort() {
    return id;
  }

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

class _DataWidgetState extends State<DataWidget> {
  /* Here the example state information is just a color, but in the full version
  of this problem this actually has multiple fields */

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: GestureDetector(
        // onDoubleTap this passes itself to the delete method of the parent
        onDoubleTap: () => widget.onDoubleTap(widget),
        // Changes color on tap to whatever color it isn't
        onTap: () {
          setState(() {
            widget.color =
                widget.color == Colors.red ? Colors.blue : Colors.red;
          });
        },
        child: Container(
          child: Center(child: Text(widget.id)),
          width: 200,
          height: 200,
          color: widget.color,
        ),
      ),
    );
  }
}
OMi Shah
  • 5,768
  • 3
  • 25
  • 34