Why do two UniqueKeys still trigger a "Multiple widgets use the same GlobalKey" assertion?

287

You have several issues with your approach.

First - as I mentioned in my comment - you are not assigning the key properly. Instead of passing the constructor argument 'key', you actually did override the key property with your own.

Additionally, in this code you are adding your addSectionButton multiple times - and each time it has the same Key - which will cause a problem:

List<Widget> sectionsToBeDisplayed = [];
    for (Widget actualSection in sections) {
      sectionsToBeDisplayed.add(actualSection);
      sectionsToBeDisplayed.add(addSectionButton);
    }

But the main problem is - your approach is wrong in several ways.

You are trying to maintain list of Widgets to be reordered. You should be reordering the data. Let the widgets rebuild on their own. Your StatefullWidget is tracking if it is selected or not, and you try to sync all the widgets once the selection changes. Instead, you should - again - be focusing on your data. Let the widgets rebuild on their own.

Here's the solution that works - you can try it in DartPad.

You will see key changes: -I introduced a List to track your ListTile titles and the text you edit -New variable to track the selected list item -onReorder is almost exactly the same as Flutter doc - I only added few lines to track the selected item -everything is in a single widget. You could still extract your Section widget (and pass a callback function for it to post changes back) - but your Section widget should be stateless.

import 'package:flutter/material.dart';

const 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 StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  
  
  final _items=<String> ["First Section"];
  final _itemsText=<String> ["First Section Text"];
  int _selectedIndex=-1;
   
  void _setSelectedIndex(int? value) {
    setState(() {
      _selectedIndex=value ?? -1;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.primary.withOpacity(0.15);

    Widget addSectionButton = Padding(
      key: const Key("__ADD_SECTION_BUTTON_KEY__"),
      padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
      child: InkWell(
        onTap: () => setState(() {
          _items.add("New Section ${_items.length+1}");
          _itemsText.add("");
        }),
        child: Container(
          height: 90,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(16), 
            border: Border.all(color: Theme.of(context).primaryColor, width: 2),
          ),
          child: Center(
            child: Icon(Icons.add, color: Theme.of(context).primaryColor, size: 40,),
          ),
        ),
      ),
    );
    
    Widget editor;
    
    if ((_selectedIndex < 0 && _selectedIndex != -1) || _selectedIndex > _items.length) {
      editor=Text("""An internal error has occured. Namely, newSelected is $_selectedIndex,
                        which is something that normally shouldn't happen.""");
    } else if (_selectedIndex == -1) {
      editor=const Text("Select a section to begin writing.");
    } else {
      
      TextEditingController _controller=TextEditingController(text: _itemsText[_selectedIndex]);
      editor=TextField(
        decoration: InputDecoration(
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
          hintText: "Once upon a time...",
        ),
        controller: _controller,
        onSubmitted: (String value) {
          _itemsText[_selectedIndex]=value;
        }
      );
    }

    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(32.0),
        child: Row(
          children: [
            Flexible(
              flex: 1,
              child: 
                ReorderableListView(
                  buildDefaultDragHandles: true,
                  onReorder: (int oldIndex, int newIndex) {
                    setState(() {
                      if (oldIndex < newIndex) {
                        newIndex -= 1;
                      }
                      final String item = _items.removeAt(oldIndex);
                      _items.insert(newIndex, item);
                      
                      final String itemText = _itemsText.removeAt(oldIndex);
                      _itemsText.insert(newIndex, itemText);
                      
                      if (_selectedIndex==oldIndex) {
                        _selectedIndex=newIndex;
                      }
                    });
                  },
                  children: <Widget>[
                    for (int index = 0; index < _items.length; index++)
                           RadioListTile(
                            key: Key('$index'),
                            value: index,
                            groupValue: _selectedIndex,
                            tileColor: index.isOdd ? oddItemColor : evenItemColor,
                            title: Text(_items[index]),
                            onChanged: _setSelectedIndex
                          ),
                          addSectionButton
                  ],
                ),
              //),
            ),
            const VerticalDivider(
              indent: 20,
              endIndent: 20,
              color: Colors.grey,
              width: 20,
            ),
            Flexible(
              flex: 2, 
              child: Card(
                color: Theme.of(context).primaryColorLight, 
                child: Center(
                  child: editor
                )
              )
            )
          ],
        )
      ),
    );
  }
}
Share:
287
WalrusGumboot
Author by

WalrusGumboot

Updated on January 01, 2023

Comments

  • WalrusGumboot
    WalrusGumboot over 1 year

    I'm trying to make a reorderable list of custom widgets, so I use a UniqueKey() in their constructors (seeing as there's not really anything else to differentiate them by). However when I go to create another element, I get the Multiple widgets use the same GlobalKey assertion. I took a look at the Widget Inspector and both of the widgets have the UniqueKey but somehow also a key of null? I suspect this is the origin of the issue but I can't figure out how to solve it.

    key is somehow also null?

    I have a few other properties in the constructor but all of them are late and can't be used in the constructor of a ValueKey.

    My code:

    section.dart:

    class Section extends StatefulWidget {
    
      final String title;
      late int index;
      bool selected = false;
    
      Section ({required this.title, required this.index});
      
      Key key = UniqueKey();
      
      @override
      SectionState createState() => SectionState();
    
      void deselect() {
        this.selected = false;
      }
    }
    

    main.dart:

    class WritingPageState extends State<WritingPage> {
      List<Section> sections = [Section(index: 0, title: "First Section")];
      ValueNotifier<int> selectedIndex = ValueNotifier(-1);
      int newKeyConcatter = 1;
    
      Widget build(BuildContext context) {
        Widget addSectionButton = Padding(
          key: Key("__ADD_SECTION_BUTTON_KEY__"),
          padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
          child: InkWell(
            onTap: () => setState(() {
              sections.add(Section(index: sections.length, title: "New Section $newKeyConcatter",));
              newKeyConcatter++;
            }),
            child: Container(
              height: 90,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(16), 
                border: Border.all(color: Theme.of(context).primaryColor, width: 2),
                //color: Theme.of(context).primaryColorLight
              ),
              child: Center(
                child: Icon(Icons.add, color: Theme.of(context).primaryColor, size: 40,),
              ),
            ),
          ),
        );
    
        List<Widget> sectionsToBeDisplayed = [];
        for (Widget actualSection in sections) {
          sectionsToBeDisplayed.add(actualSection);
          sectionsToBeDisplayed.add(addSectionButton);
        }
    
    
    
    
        return Scaffold(
          body: Padding(
            padding: const EdgeInsets.all(32.0),
            child: Row(
              children: [
                Flexible(
                  flex: 1,
                  child: NotificationListener<SectionSelectedNotification> (
                    onNotification: (notif) {
                      setState(() {
                        for (Section s in sections) {
                          if (s.index != notif.index) {s.deselect();}
                        }                    
                      });
                      return true;
                    },
                    child: ReorderableListView(
                      buildDefaultDragHandles: false,
                      onReorder: (int oldIndex, int newIndex) {
                        setState(() {
                          if (oldIndex < newIndex) {
                            newIndex -= 1;
                          }
                          final Section item = sections.removeAt(oldIndex);
                          sections.insert(newIndex, item);
                  
                          for (int i = 0; i < sections.length; i++) {
                            sections[i].index = i;
                          }
                        });
                      },
                      children: sectionsToBeDisplayed,
                    ),
                  ),
                ),
                VerticalDivider(
                  indent: 20,
                  endIndent: 20,
                  color: Colors.grey,
                  width: 20,
                ),
                Flexible(
                  flex: 2, 
                  child: Card(
                    color: Theme.of(context).primaryColorLight, 
                    child: Center(
                      child: ValueListenableBuilder(
                        valueListenable: selectedIndex,
                        builder: (BuildContext context, int newSelected, Widget? child) {
                          if ((newSelected < 0 && newSelected != -1) || newSelected > sections.length) {
                            return Text("""An internal error has occured. Namely, newSelected is $newSelected,
                            which is something that normally shouldn't happen.""");
                          } else if (newSelected == -1) {
                            return Text("Select a section to begin writing.");
                          }
    
                          return TextField(
                            decoration: InputDecoration(
                              border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
                              hintText: "Once upon a time...",
                              
                            ),
                          );
                        },
                      )
                    )
                  )
                )
              ],
            )
          ),
        );
      }
    }
    
    • Andrija
      Andrija over 2 years
      There is key property that is part of the Widget - this is the one that has the value of null, because you never initiated it. What you did - you created your own property 'key' and initialized that. You should have something like Section ({required this.title, required this.index, Key key}) : super(key: key); . This will initialize the Widget key when you create it like this: List<Section> sections = [Section(index: 0, title: "First Section"), key: UniqueKey()];. Try with this, see what happens. One more thing - selected and deselect() should be in your state object, not here.
    • Andrija
      Andrija over 2 years