skip to Main Content

Below is my minimal reproduction of my working code, where a dynamic list is created. The list has been initialised with a single element at the start. User can add more items on pressing a button or user can dismiss the item by swiping from end to start.

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late List<InvoiceItemInput> itemsList;

  @override
  void initState() {
    itemsList = List<InvoiceItemInput>.from([
      InvoiceItemInput(
        parentWidth: 400.0,
        index: 1,
        key: ValueKey('1' + DateTime.now().toString()),
      )
    ]);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SingleChildScrollView(
          child: Column(
            children: [
              ListView.builder(
                shrinkWrap: true,
                itemBuilder: (BuildContext context, int index) => Dismissible(
                    key: ValueKey(index.toString() + DateTime.now().toString()),
                    child: itemsList[index],
                    background: Container(color: Colors.red),
                    direction: DismissDirection.endToStart,
                    onDismissed: (direction) {
                      if (direction == DismissDirection.endToStart) {
                        setState(() {
                          itemsList.removeAt(index);
                        });
                      }
                    }),
                itemCount: itemsList.length,
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 20.0),
                child: Align(
                  alignment: Alignment.centerRight,
                  child: OutlinedButton(
                      child: Text('ADD'),
                      onPressed: () {
                        setState(() {
                          final int index = itemsList.length;
                          itemsList.add(InvoiceItemInput(
                            parentWidth: 400.0,
                            index: index + 1,
                            key: ValueKey(
                                index.toString() + DateTime.now().toString()),
                          ));
                        });
                      }),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class InvoiceItemInput extends StatefulWidget {
  const InvoiceItemInput(
      {super.key, required this.parentWidth, required this.index});
  final double parentWidth;
  final int index;

  @override
  State<InvoiceItemInput> createState() => _InvoiceItemInputState();
}

class _InvoiceItemInputState extends State<InvoiceItemInput> {
  late TextEditingController? itemController;
  final double horizontalSpacing = 15.0;
  bool showDeleteButton = false;

  @override
  void initState() {
    itemController = TextEditingController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(height: 12.0),
        Container(
          padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 7.0),
          child: Text('Item ${widget.index}',
              style: Theme.of(context)
                  .textTheme
                  .labelLarge
                  ?.copyWith(color: Colors.white)),
          decoration: BoxDecoration(
              color: Colors.lightBlue,
              borderRadius: BorderRadius.circular(7.0)),
        ),
        SizedBox(height: 7.0),
        Wrap(
          spacing: this.horizontalSpacing,
          runSpacing: 25.0,
          children: [
            SizedBox(
              width: (widget.parentWidth - horizontalSpacing) / 2 < 200.0
                  ? (widget.parentWidth - horizontalSpacing) / 2
                  : 200.0,
              child: DropdownMenu(
                controller: itemController,
                label: Text(
                  'Item Name *',
                  style: const TextStyle(
                      fontFamily: 'Raleway',
                      fontSize: 14.0,
                      color: Colors.black87,
                      fontWeight: FontWeight.w500),
                ),
                hintText: 'Enter Item Name',
                requestFocusOnTap: true,
                enableFilter: true,
                expandedInsets: EdgeInsets.zero,
                textStyle: Theme.of(context).textTheme.bodySmall,
                menuStyle: MenuStyle(
                  backgroundColor: WidgetStateProperty.all(Colors.lightBlue),
                ),
                dropdownMenuEntries: [
                  DropdownMenuEntry(
                    value: 'Pen',
                    label: 'Pen',
                    style: MenuItemButton.styleFrom(
                      foregroundColor: Colors.white,
                      textStyle: Theme.of(context).textTheme.bodySmall,
                    ),
                  ),
                  DropdownMenuEntry(
                    value: 'Pencil',
                    label: 'Pencil',
                    style: MenuItemButton.styleFrom(
                      foregroundColor: Colors.white,
                      textStyle: Theme.of(context).textTheme.bodySmall,
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
        SizedBox(height: 15.0),
      ],
    );
  }
}

Problem:
When an item is added or dismissed, the changes are lost in the TextField / DropdownMenu.

Looking for:
Could you please suggest a way out so that the Item index shown in a Container at top of list item also updates to new value but keeping the user input intact.

2

Answers


  1. Chosen as BEST ANSWER

    Building upon @Altay 's response, my answer solves updating index of an item problem too. I still have a question mentioned as a comment in the code below. Do we need to dispose removed TextControllers() from the list? how?

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatefulWidget {
      const MyApp({super.key});
      @override
      State<MyApp> createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      List<ValueKey> itemKeys = [];
      List<Map<String, dynamic>> itemsControllers = [];
      late List<InvoiceItemInput> itemsList;
    
      @override
      void initState() {
        super.initState();
        itemKeys.add(ValueKey('1' + DateTime.now().toString()));
        itemsControllers.add({
          'key': itemKeys[0],
          'itemControllers':
              Set<TextEditingController>.unmodifiable([TextEditingController()]) // Using Set here in case we have multiple text controllers here
        });
        itemsList = List<InvoiceItemInput>.from([
          InvoiceItemInput(
            parentWidth: 400.0,
            index: 1,
            key: itemKeys[0],
            controllers: itemsControllers[0]['itemControllers']!,
          )
        ]);
      }
    
      @override
      void dispose() {
        for (int k = 0; k < itemsControllers.length; k++) {
          itemsControllers[k]['itemControllers']
              .forEach((controller) => controller.dispose());
        }
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: SingleChildScrollView(
              child: Column(
                children: [
                  ListView.builder(
                    shrinkWrap: true,
                    itemBuilder: (BuildContext context, int index) => Dismissible(
                        key: ValueKey(index.toString() + DateTime.now().toString()),
                        child: itemsList[index],
                        background: Container(color: Colors.red),
                        direction: DismissDirection.endToStart,
                        onDismissed: (direction) {
                          if (direction == DismissDirection.endToStart) {
                            final removeKey = itemKeys.elementAt(index);
                            itemKeys.removeAt(index);
                            itemsList.removeWhere(
                                (invoiceItem) => invoiceItem.key == removeKey);
                            itemsControllers.removeWhere(
                                (itemControl) => itemControl['key'] == removeKey);  // Question: Do we need to dispose these controllers removed, if so how can we do ?
                            itemsList.indexed
                                .forEach(((int, InvoiceItemInput) item) {
                              if (item.$2.index > item.$1 + 1) {
                                final replacementItem = InvoiceItemInput(
                                  parentWidth: 400.0,
                                  index: item.$1 + 1,
                                  key: item.$2.key,
                                  controllers: item.$2.controllers,
                                );
                                itemsList.removeAt(item.$1);
                                itemsList.insert(item.$1, replacementItem);
                              }
                            });
                            setState(() {});
                          }
                        }),
                    itemCount: itemsList.length,
                  ),
                  Padding(
                    padding: EdgeInsets.symmetric(vertical: 20.0),
                    child: Align(
                      alignment: Alignment.centerRight,
                      child: OutlinedButton(
                          child: Text('ADD'),
                          onPressed: () {
                            final int index = itemsList.length;
                            itemKeys.add(ValueKey(
                                index.toString() + DateTime.now().toString()));
                            itemsControllers.add({
                              'key': itemKeys[index],
                              'itemControllers':
                                  Set<TextEditingController>.unmodifiable(
                                      [TextEditingController()])
                            });
                            setState(() {
                              itemsList.add(InvoiceItemInput(
                                parentWidth: 400.0,
                                index: index + 1,
                                key: itemKeys[index],
                                controllers: itemsControllers[index]
                                    ['itemControllers'],
                              ));
                            });
                          }),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class InvoiceItemInput extends StatefulWidget {
      const InvoiceItemInput(
          {Key? key,
          required this.parentWidth,
          required this.index,
          required this.controllers})
          : super(key: key);
      final double parentWidth;
      final int index;
      final Set<TextEditingController> controllers;
    
      @override
      State<InvoiceItemInput> createState() => _InvoiceItemInputState();
    }
    
    class _InvoiceItemInputState extends State<InvoiceItemInput> {
      final double horizontalSpacing = 15.0;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(height: 12.0),
            Container(
              padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 7.0),
              child: Text('Item ${widget.index}',
                  style: Theme.of(context)
                      .textTheme
                      .labelLarge
                      ?.copyWith(color: Colors.white)),
              decoration: BoxDecoration(
                  color: Colors.lightBlue,
                  borderRadius: BorderRadius.circular(7.0)),
            ),
            SizedBox(height: 7.0),
            Wrap(
              spacing: this.horizontalSpacing,
              runSpacing: 25.0,
              children: [
                SizedBox(
                  width: (widget.parentWidth - horizontalSpacing) / 2 < 200.0
                      ? (widget.parentWidth - horizontalSpacing) / 2
                      : 200.0,
                  child: TextField(
                    controller: widget.controllers.elementAt(0),
                    decoration: InputDecoration(
                      labelText: 'Item Name *',
                      hintText: 'Enter Item Name',
                    ),
                  ),
                ),
              ],
            ),
            SizedBox(height: 15.0),
          ],
        );
      }
    }
    

  2. Each time the state is refreshed, the InvoiceItemInputs are recreated. Naturally, the TextEditingControllers you defined inside are also recreated. This seems to be the source of the problem.

    Remember: the setState method triggers the build method of the associated StatefulWidget.

    In this case, if you want to prevent your data from being lost, you should define the controllers outside.

    Below, I will provide a suggestion:

    class _MyAppState extends State<MyApp> {
      late List<InvoiceItemInput> itemsList;
      List<TextEditingController> itemControllers = [];
    
      @override
      void initState() {
        super.initState();
        final controller = TextEditingController();
        itemsList = List<InvoiceItemInput>.from([
          InvoiceItemInput(
            parentWidth: 400.0,
            index: 1,
            controller: controller,
            key: ValueKey('1' + DateTime.now().toString()),
          )
        ]);
        itemControllers.add(controller);
      }
    
      @override
      void dispose() {
        for (var controller in itemControllers) {
          controller.dispose();
        }
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: SingleChildScrollView(
              child: Column(
                children: [
                  ListView.builder(
                    shrinkWrap: true,
                    itemBuilder: (BuildContext context, int index) => Dismissible(
                        key: ValueKey(index.toString() + DateTime.now().toString()),
                        child: itemsList[index],
                        background: Container(color: Colors.red),
                        direction: DismissDirection.endToStart,
                        onDismissed: (direction) {
                          if (direction == DismissDirection.endToStart) {
                            setState(() {
                              itemsList.removeAt(index);
                              itemControllers.removeAt(index);
                            });
                          }
                        }),
                    itemCount: itemsList.length,
                  ),
                  Padding(
                    padding: EdgeInsets.symmetric(vertical: 20.0),
                    child: Align(
                      alignment: Alignment.centerRight,
                      child: OutlinedButton(
                          child: Text('ADD'),
                          onPressed: () {
                            setState(() {
                              final int index = itemsList.length;
                              var controller = TextEditingController();
                              itemControllers.add(controller);
                              itemsList.add(InvoiceItemInput(
                                parentWidth: 400.0,
                                index: index + 1,
                                controller: controller,
                                key: ValueKey(
                                    index.toString() + DateTime.now().toString()),
                              ));
                            });
                          }),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class InvoiceItemInput extends StatefulWidget {
      const InvoiceItemInput(
          {super.key,
          required this.parentWidth,
          required this.index,
          required this.controller});
      final double parentWidth;
      final int index;
      final TextEditingController controller;
    
      @override
      State<InvoiceItemInput> createState() => _InvoiceItemInputState();
    }
    
    class _InvoiceItemInputState extends State<InvoiceItemInput> {
      final double horizontalSpacing = 15.0;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(height: 12.0),
            Container(
              padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 7.0),
              child: Text('Item ${widget.index}',
                  style: Theme.of(context)
                      .textTheme
                      .labelLarge
                      ?.copyWith(color: Colors.white)),
              decoration: BoxDecoration(
                  color: Colors.lightBlue,
                  borderRadius: BorderRadius.circular(7.0)),
            ),
            SizedBox(height: 7.0),
            Wrap(
              spacing: this.horizontalSpacing,
              runSpacing: 25.0,
              children: [
                SizedBox(
                  width: (widget.parentWidth - horizontalSpacing) / 2 < 200.0
                      ? (widget.parentWidth - horizontalSpacing) / 2
                      : 200.0,
                  child: TextField(
                    controller: widget.controller,
                    decoration: InputDecoration(
                      labelText: 'Item Name *',
                      hintText: 'Enter Item Name',
                    ),
                  ),
                ),
              ],
            ),
            SizedBox(height: 15.0),
          ],
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search