1

I'm struggling with this for few days, but can't figure it out. I have a simple widget, where I want to dynamically add and remove inputs (for sets/reps). The problem is, if I'm removing let's say the first item, the last item gets removed from UI.

I've created a dartpad for the app: dartpad

My main.dart:

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SetBuilderWidget();
  }
}

class TempSet {
  int weight;
  int rep;
  int order;

  TempSet(this.order, this.weight, this.rep);
}

class SetBuilderWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SetBuilderWidgetState();
}

class _SetBuilderWidgetState extends State<SetBuilderWidget> {
  final sets = [];
  final List<TempSet> tempSet = [];

  Widget _row(int index) {
    return Row(
      children: <Widget>[
        Text("Set $index"),
        Flexible(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 40),
            child: TextField(
              decoration: InputDecoration(
                labelText: "Rep count",
              ),
            ),
          ),
        ),
        IconButton(
          icon: Icon(Icons.delete),
          onPressed: () => _removeSet(index),
        ),
      ],
    );
  }

  List<Widget> _rows() {
    return tempSet.map((s) => _row(s.order)).toList();
  }

  void _addSet() {
    if (tempSet.length == 0) {
      tempSet.add(TempSet(0, null, null));
    } else {
      final lastSet = tempSet.last;
      tempSet.add(TempSet(tempSet.indexOf(lastSet) + 1, null, null));
    }
  }

  void _removeSet(int index) {
    setState(() {
      tempSet.removeAt(index);
      _updateIndexes();
    });
  }

  void _updateIndexes() {
    tempSet.asMap().forEach((index, s) {
      s.order = index;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ..._rows(),
        RaisedButton(
          onPressed: () {
            setState(() {
              _addSet();
            });
          },
          child: Text("Add set"),
        )
      ],
    );
  }
}

Issue:

enter image description here

Runtime Terror
  • 6,242
  • 11
  • 50
  • 90

2 Answers2

5

You should save the state of those TextFields in your map.

Solution

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SetBuilderWidget();
  }
}

class TempSet {
  int weight;
  int rep;
  int order;

  TempSet(this.order, this.weight, this.rep);
}

class SetBuilderWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SetBuilderWidgetState();
}

class _SetBuilderWidgetState extends State<SetBuilderWidget> {
  final sets = [];
  final List<TempSet> tempSet = [];

  Widget _row(int index, int rep) {
    TextEditingController _controller =
        TextEditingController(text: rep != null ? rep.toString() : "0");

    return Row(
      children: <Widget>[
        Text("Set $index"),
        Flexible(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 40),
            child: TextField(
              onChanged: (text) {
                tempSet[index].rep = num.parse(text).toInt();
                print(tempSet);
              },
              controller: _controller,
              decoration: InputDecoration(
                labelText: "Rep count",
              ),
            ),
          ),
        ),
        IconButton(
          icon: Icon(Icons.delete),
          onPressed: () => _removeSet(index),
        ),
      ],
    );
  }

  List<Widget> _rows() {
    return tempSet.map((s) => _row(s.order, s.rep)).toList();
  }

  void _addSet() {
    if (tempSet.length == 0) {
      tempSet.add(TempSet(0, null, null));
    } else {
      final lastSet = tempSet.last;
      tempSet.add(TempSet(tempSet.indexOf(lastSet) + 1, null, null));
    }
  }

  void _removeSet(int index) {
    setState(() {
      tempSet.removeAt(index);
      _updateIndexes();
    });
  }

  void _updateIndexes() {
    tempSet.asMap().forEach((index, s) {
      s.order = index;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ..._rows(),
        RaisedButton(
          onPressed: () {
            setState(() {
              _addSet();
            });
          },
          child: Text("Add set"),
        )
      ],
    );
  }
}

By the way you should avoid using widget functions. Declare them as classes instead.

easeccy
  • 4,248
  • 1
  • 21
  • 37
2

Your code has a series of issues. Starting with the fact that you are using a Column for a list of items that can grow larger than your view and with a Column you will not be able to scroll it. This also means that your Add set button can eventually become hidden. Using a ListView.builder would be the most appropriate approach to what you want to build and a FloatingActionButton for the Add set button. Please check the code I've written below for an example of the implementation I propose based on your original code and purpose:

class MainExerciseApp extends StatefulWidget {
  @override
  _MainExerciseAppState createState() => _MainExerciseAppState();
}

class _MainExerciseAppState extends State<MainExerciseApp> {
  int currentIndex = 0;
  List<ExerciseSet> setList = [];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: Color.fromARGB(255, 18, 32, 47)),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: SetList(
            setList: setList,
            removeSet: _removeSet,
          ),
        ),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: _addSet
        ),
      ),
    );
  }

  void _addSet(){
    currentIndex++;
    setState(() {
      setList.add(ExerciseSet(
        index: currentIndex,
        reps: 0
      ));
    });
  }

  void _removeSet(ExerciseSet exerciseSet){
    setState(() {
      setList.remove(exerciseSet);
    });
  }
}


class SetList extends StatefulWidget {
  final List<ExerciseSet> setList;
  final Function removeSet;

  SetList({
    @required this.setList,
    @required this.removeSet,
  });

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

class _SetListState extends State<SetList> {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: widget.setList.length,
      itemBuilder: (context, index) {
        return Row(
          children: <Widget>[
            Text("Set ${widget.setList[index].index.toString()}"),
            Flexible(
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 40),
                child: TextFormField(
                  initialValue: widget.setList[index].reps.toString(),
                  decoration: InputDecoration(
                    labelText: "Rep count",
                  ),
                ),
              ),
            ),
            IconButton(
              icon: Icon(Icons.delete),
              onPressed: () => widget.removeSet(widget.setList[index]),
            ),
          ],
        );
      }
    );
  }
}

class ExerciseSet {
  int index;
  double weight;
  int reps;

  ExerciseSet({
    @required this.index,
    this.weight,
    this.reps,
  });
}

There are things that Flutter and Dart take care of for you and make your life easier. If you want to learn a bit more on how to build a manageable list like this, you can check this GitHub example.

J. S.
  • 8,905
  • 2
  • 34
  • 44