Christer
Christer

Reputation: 3076

Flutter ListView widgets resets icon when scrolling items out of screen

I have a ListView with custom statefull widgets as items. These items has an onTap event that switch the icon from check_box_outline_blank to check_box, and the other way around.

However when I select an item in the list and scroll down and back up again the item has reset the icon from check_box to check_box_outline_blank.

How can I prevent this from happening?

            new Expanded(
                child: ListView.builder(
                  itemCount: _list.length,
                  itemBuilder: (BuildContext context, int index) {
                    return listTile(_pveList[index], index)
                  },
                )
            )

 //Adds extra space(height:100) after the last tile
 StatefulWidget listTile(String text, int index){
    return (index < _pveList.length - 1)?new SelectableListItem(text: text,
      onTap: (){
        _handleOnTap(text);
      },
    ): new Column(
      children: <Widget>[
        new SelectableListItem(text: text,
          onTap: (){
            _handleOnTap(text);
          },
        ),
        SizedBox(
          height: 100,
        )
      ],
    );
  }

The tile widget(SelectableListItem):

import 'package:flutter/material.dart';

class SelectableListItem extends StatefulWidget {
  SelectableListItem({Key key, this.text, this.onTap})
      : super(key: key);
  final String text;
  final GestureTapCallback onTap;

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

class _SelectableListItemState extends State<SelectableListItem> {

  bool isSelected = false;
  Icon _checked = new Icon(Icons.check_box_outline_blank);

  handleOnTap() {
    isSelected = !isSelected;
    if (isSelected) {
      setState(() {
        _checked = new Icon(Icons.check_box);
      });
    } else {
      setState(() {
        _checked = new Icon(Icons.check_box_outline_blank);
      });
    }
    return widget.onTap();
  }

  @override
  Widget build(context) {
    // Pass the text down to another widget
    return new GestureDetector(
      behavior: HitTestBehavior.translucent,
      onTap: () => handleOnTap(),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          new Padding(
            padding: EdgeInsets.all(22),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                new Text(widget.text, style: TextStyle(fontSize: 18)),
                _checked
              ],
            ),
          ),
          new Container(
            width: double.infinity,
            height: 1,
            color: Color(0x1a222222),
          )
        ],
      ),
    );
  }
}

Upvotes: 1

Views: 1474

Answers (1)

Egor
Egor

Reputation: 10334

Updated

I believe the OP wants to achieve a standard Checkbox with a label text aside.

CheckboxListTile widget provides such functionality.

Usage example:

bool selected = false;

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(children: <Widget>[

      CheckboxListTile(
        onChanged: (val) {
          setState(() {
            selected = !selected;
          });
        },
        value: selected,
        title: Text("Test"),
      )

    ]),
  );
}

If you want to have the checkbox located on the left side of the screen - set controlAffinity property accordingly:

CheckboxListTile(
  // ...,
  controlAffinity: ListTileControlAffinity.leading,
)

Original answer

#1

You should never use functions to build the widget tree.

See this answer for more details.

#2

I'd recommend to use ValueNotifier, declared in SelectableListItem class.

It allows you to change value of a variable in StatefulWidget without the usual warnings for non-final variables.

The value inside of ValueNotifier gets preserved too, just like any other variables that are declared in StatefulWidget class.

e.g.

class SelectableListItem extends StatefulWidget {
  // ...
  final ValueNotifier<bool> isSelected = ValueNotifier<bool>(false);
  // ...
}

class _SelectableListItemState extends State<SelectableListItem> {

  handleOnTap() {
    setState(() {
      widget.isSelected.value = !widget.isSelected.value;
    });
    return widget.onTap();
  }

  // ... build() { ...
  Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: <Widget>[
      Text(widget.text, style: TextStyle(fontSize: 18)),
      Icon(widget.isSelected.value ? Icons.check_box : Icons.check_box_outline_blank),
    ]
  ),
  // ... } ...

}

ValueNotifier<bool> also eliminates the need to store the icon widget in a separate variable. Instead, you can use it in inline boolean condition, just like it's shown in the code.

PS: I did not test the code, however general instructions are valid. Let me know if this helped.

Upvotes: 1

Related Questions