Search code examples
flutterlistviewdartfocusbloc

Deleting specific item out of ListView with Bloc


I have a page that consists of a ListView, which contains TextFormFields. The user can add or remove items from that ListView. I use the bloc pattern, and bind the number of Items and their content inside the ListView to a list saved in the bloc state. When I want to remove the items, I remove the corresponding text from this list and yield the new state. However, this will always remove the last item, instead of the item that's supposed to be removed. While debugging, I can clearly see that the Item I want removed is in fact removed from the state's list. Still, the ListView removes the last item instead.

I've read that using keys solves this problem and it does. However, if I use keys there is a new problem. Now, the TextFormField will go out of focus every time a character is written. I guess this is to do with the fact that the ListView is redrawing its items everytime a character is typed, and somehow having a key makes the focus behave differently.

Any ideas how to solve this?

The page code (The ListView is at the bottom):

class GiveBeneftis extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var bloc = BlocProvider.of<CreateChallengeBloc>(context);
    return BlocBuilder<CreateChallengeBloc, CreateChallengeState>(
        builder: (context, state) {
      return CreatePageTemplate(
        progress: state.progressOfCreation,
        buttonBar: NavigationButtons(
          onPressPrevious: () {
            bloc.add(ProgressOfCreationChanged(nav_direction: -1));
            Navigator.of(context).pop();
          },
          onPressNext: () {
            bloc.add(ProgressOfCreationChanged(nav_direction: 1));
            Navigator.of(context).pushNamed("create_challenge/add_pictures");
          },
          previous: 'Details',
          next: 'Picture',
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Text(
              'List the benefits of you Challenge',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 30),
            Text(
              'Optionally: Make a list of physical and mental benefits the participants can expect. ',
              textAlign: TextAlign.center,
              style: TextStyle(
                  color: Colors.grey,
                  fontSize: 14,
                  fontWeight: FontWeight.w400),
            ),
            SizedBox(height: 50),
            Container(
              margin: EdgeInsets.all(8.0),
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(12),
                  color: Colors.yellow[600]),
              child: FlatButton(
                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                onPressed: () => bloc.add(ChallengeBenefitAdded()),
                child: Text('Add a benefit',
                    style: TextStyle(
                        color: Colors.white, fontWeight: FontWeight.bold)),
              ),
            ),
            Expanded(
                child: new ListView.builder(
                    itemCount: state.benefits.length,
                    itemBuilder: (BuildContext context, int i) {
                      final item = state.benefits[i];
                      return Padding(
                          padding: EdgeInsets.symmetric(horizontal: 25),
                          child: TextFieldTile(
                            //key: UniqueKey(),
                            labelText: 'Benefit ${i + 1}',
                            validator: null,
                            initialText: state.benefits[i],
                            onTextChanged: (value) => bloc.add(
                                ChallengeBenefitChanged(
                                    number: i, text: value)),
                            onCancelIconClicked: () {
                              bloc.add(ChallengeBenefitRemoved(number: i));
                            },
                          ));
                    })),
          ],
        ),
      );
    });
  }
}

The Code of the TextfieldTile:

class TextFieldTile extends StatelessWidget {
  final Function onTextChanged;
  final Function onCancelIconClicked;
  final Function validator;
  final String labelText;
  final String initialText;

  const TextFieldTile(
      {Key key,
      this.onTextChanged,
      this.onCancelIconClicked,
      this.labelText,
      this.initialText,
      this.validator})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Stack(children: <Widget>[
      TextFormField(
          textCapitalization: TextCapitalization.sentences,
          initialValue: initialText,
          validator: validator,
          onChanged: onTextChanged,
          maxLines: null,
          decoration: InputDecoration(
            labelText: labelText,
          )),
      Align(
        alignment: Alignment.topRight,
        child: IconButton(
            icon: Icon(Icons.cancel), onPressed: onCancelIconClicked),
      ),
    ]);
  }
}

The relevant portion of the Bloc:

 if (event is ChallengeBenefitAdded) {
      var newBenefitsList = List<String>.from(state.benefits);
      newBenefitsList.add("");
      yield state.copyWith(benefits: newBenefitsList);
    }
    else if (event is ChallengeBenefitChanged) {
      var newBenefitsList = List<String>.from(state.benefits);
      newBenefitsList[event.number] = event.text;
      yield state.copyWith(benefits: newBenefitsList);
    }
    else if (event is ChallengeBenefitRemoved) {
      var newBenefitsList = List<String>.from(state.benefits);
      newBenefitsList.removeAt(event.number);
      yield state.copyWith(benefits: newBenefitsList);
    }

Solution

  • I can think of two things you can do here.

    1. Create a different bloc for processing the changes in the text field, that will avoid having to actually update the state of the entire list if no needed.
    2. Have a conditional to avoid rebuilding the list when your bloc change to a state that is relevant only to the keyboard actions.

    Example:

    BlocBuilder<CreateChallengeBloc, CreateChallengeState>(
        buildWhen: (previousState, currentState) {
            return (currentState is YourNonKeyboardStates);
         }
         ...
    );