Related
I am using DropdownButton and I am facing the following issue. I'm using a checkbox in elements, but when I click on an element, I don't get a checkmark indicating that the checkbox has been clicked. As a result, I need to close and reopen it, and then I will see the changes that were clicked on the "checkbox". The second problem is that when I select one element, all elements are selected for me. As a final result, I need to get so that I can select an element and the checkbox is immediately marked, if 2 elements are needed, then two, and so on. Tell me how to fix these problems, I will be grateful for the help?
dropdown
class DropdownWidget extends StatefulWidget {
List<String> items;
SvgPicture? icon;
double width;
DropdownWidget({
Key? key,
required this.items,
required this.icon,
required this.width,
}) : super(key: key);
#override
State<DropdownWidget> createState() => _DropdownWidgetState();
}
class _DropdownWidgetState extends State<DropdownWidget> {
String? selectedValue;
bool isChecked = false;
#override
void initState() {
super.initState();
if (widget.items.isNotEmpty) {
selectedValue = widget.items[1];
}
}
#override
Widget build(BuildContext context) {
return SizedBox(
width: widget.width,
child: DropdownButtonHideUnderline(
child: DropdownButton2(
items: widget.items
.map((item) => DropdownMenuItem<String>(
value: item,
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: constants.Colors.white.withOpacity(0.1),
width: 1,
),
),
),
child: Center(
child: Row(
children: [
if (item == selectedValue)
const SizedBox(
width: 0,
),
Expanded(
child: Text(
item,
style: constants.Styles.smallTextStyleWhite,
),
),
Checkbox(
checkColor: Colors.black,
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
),
],
),
),
),
))
.toList(),
value: selectedValue,
onChanged: (value) {
setState(() {
selectedValue = value as String;
});
},
icon: SvgPicture.asset(constants.Assets.arrowDropdown),
iconSize: 21,
buttonHeight: 27,
itemHeight: 47,
dropdownMaxHeight: 191,
dropdownWidth: 140,
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: constants.Colors.purpleMain,
),
color: constants.Colors.greyDark,
),
selectedItemBuilder: (context) {
return widget.items.map(
(item) {
return Row(
children: [
widget.icon ?? const SizedBox(),
const SizedBox(width: 8),
Text(
item,
style: constants.Styles.bigBookTextStyleWhite,
),
],
);
},
).toList();
},
),
),
);
}
}
items
final List<String> items = const [
"All EV's",
'Main EV',
'<EV2>',
];
I hope this example explains the concept. For simplcity I made simple a new file, run it and see the results:
Then main idea in two lists, _checkList contain values of the CheckBox and _selectedList handles the main dropdown widget to show the selection.
Feel free to ask any questions and I'm happy to help
import 'package:flutter/material.dart';
class TestPage extends StatelessWidget {
const TestPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const AnimationDemo(number: 5);
}
}
class AnimationDemo extends StatefulWidget {
const AnimationDemo({Key? key, this.number = 2}) : super(key: key);
final int number;
#override
State<AnimationDemo> createState() => _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo> {
late List<bool> _checkList;
late List<int> _selectedIndex;
bool _isOpen = false;
#override
void initState() {
_checkList = List.filled(widget.number, false);
_selectedIndex = <int>[];
super.initState();
}
List<DropDownItem> generateItems() {
var tmp = <DropDownItem>[];
for (var i = 0; i < _checkList.length; i++) {
tmp.add(DropDownItem(
isChecked: _checkList[i],
onChanged: (value) {
setState(() {
_checkList[i] = value!;
if (value && !_selectedIndex.contains(i)) {
_selectedIndex.add(i);
} else {
_selectedIndex.remove(i);
}
});
},
));
}
return tmp;
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Text((_selectedIndex.isEmpty)
? 'Nothing Selected'
: _selectedIndex.join(',')),
),
GestureDetector(
onTap: () {
setState(() {
_isOpen = !_isOpen;
});
},
child: const Icon(Icons.arrow_downward),
),
],
),
AnimatedOpacity(
opacity: (_isOpen) ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: Column(
mainAxisSize: MainAxisSize.min,
children: generateItems(),
),
)
],
),
);
}
}
class DropDownItem extends StatelessWidget {
final bool isChecked;
final Function(bool?)? onChanged;
const DropDownItem({Key? key, this.onChanged, this.isChecked = false})
: super(key: key);
#override
Widget build(BuildContext context) {
return Row(
children: [
const Expanded(child: Text('Demo item')),
Checkbox(value: isChecked, onChanged: onChanged)
],
);
}
}
Here's how to achieve the Multiselect dropdown with DropdownButton2:
final List<String> items = [
'Item1',
'Item2',
'Item3',
'Item4',
];
List<String> selectedItems = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: DropdownButtonHideUnderline(
child: DropdownButton2(
isExpanded: true,
hint: Align(
alignment: AlignmentDirectional.center,
child: Text(
'Select Items',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).hintColor,
),
),
),
items: items.map((item) {
return DropdownMenuItem<String>(
value: item,
//disable default onTap to avoid closing menu when selecting an item
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final _isSelected = selectedItems.contains(item);
return InkWell(
onTap: () {
_isSelected
? selectedItems.remove(item)
: selectedItems.add(item);
//This rebuilds the StatefulWidget to update the button's text
setState(() {});
//This rebuilds the dropdownMenu Widget to update the check mark
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
_isSelected
? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
],
),
),
);
},
),
);
}).toList(),
//Use last selected item as the current value so if we've limited menu height, it scroll to last item.
value: selectedItems.isEmpty ? null : selectedItems.last,
onChanged: (value) {},
buttonHeight: 40,
buttonWidth: 140,
itemHeight: 40,
itemPadding: EdgeInsets.zero,
selectedItemBuilder: (context) {
return items.map(
(item) {
return Container(
alignment: AlignmentDirectional.center,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
selectedItems.join(', '),
style: const TextStyle(
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
);
},
).toList();
},
),
),
),
);
}
Also, I've added it as an example to the package doc "Example 4" so you can get back to it later.
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.
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...",
),
);
},
)
)
)
)
],
)
),
);
}
}
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
)
)
)
],
)
),
);
}
}
Hello I have a program that add a new widget to a column on button press
when i run this it should create a dropdown and a Textfield inside it and when i add values to it
i need to add the values in a separate List
So here is my code
class MedPreC extends StatefulWidget {
#override
_MedPreCState createState() => _MedPreCState();
}
//When ini med will get values from firebase no error here
List<String> med = new List<String>();
//this is used as children of column
List<Widget> containerList = [];
//initial value of dropdown
String selectedScale = "";
class _MedPreCState extends State<MedPreC> {
//When Init get value from firestore and store it into med list
initState() {
Firestore.instance.collection("Med").getDocuments().then((value) {
if (value.documents.length > 0) {
for (var i = 0; i < value.documents.length; i++) {
med.add(value.documents[i].data["Name"]);
print(med[i]);
print(med.length);
}
} else {
print("Error Med Value not found");
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 70, 20, 0),
child: Container(
child: Column(
children: [
Expanded(
child: Container(
color: Colors.white,
height: 400,
width: double.infinity,
child: SingleChildScrollView(
child: Column(
//containre list above made list of widgets
children: containerList,
),
),
),
),
RaisedButton(
child: Text("Add"),
onPressed: () {
setState(() {
//Add values into containerList
containerList.insert(0, returnWidget());
});
},
)
RaisedButton(
child: Text("Save"),
onPressed: () {
//Save the values from different container to the list rStorage
},
)
],
),
),
);
}
This is where all the problems are
Widget returnWidget() {
if (med != null) {
print(med.elementAt(1));
return Padding(
padding: const EdgeInsets.all(2.0),
child: Container(
height: 35,
color: Colors.yellow,
//This Drop Down gives me error
//And i also need to add a TextField to this container if you have time it is on an external //button() not in this function click even shuld be able to get all values and save it into another //list
child: DropdownButtonHideUnderline(
child: SizedBox(
width: double.infinity,
child: DropdownButton(
value: selectedScale,
hint: Text(' Units '),
onChanged: (value) {
setState(() {
selectedScale = value;
});
},
items: med
.map(
(e) => DropdownMenuItem(
child: new Text(e),
value: e,
),
)
.toList(),
),
),
),
),
);
} else {
print('Med list not loaded');
return Text("Medisnot loaded");
}
}
}
class Xyz {
Xyz(this.med, this.req);
final String med, req;
}
//This is where all the data is stored on button click
List<Xyz> rStorage = new List<Xyz>();
IN short i need to generate dropdown and a Text field Dynamically on a button click
And on another button click i need to save all these data which are entered into these Dd and Text field to a list(rStorage)
To do this. instead of using a function to generate those dynamic widgets use a class
and its instances. Generate new instance and store it in the container list. Now you can access Drop-down value and Text Field value by going into those instance
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class MedPreC extends StatefulWidget {
#override
_MedPreCState createState() => _MedPreCState();
}
List<String> med = new List<String>();
List<MyDynamicWidget> containerList = [];
String selectedScale = "";
List<String> myStrings = new List<String>();
class _MedPreCState extends State<MedPreC> {
initState() {
//Use your firebase or generate your own Stinglist here
Firestore.instance.collection("Med").getDocuments().then((value) {
if (value.documents.length > 0) {
for (var i = 0; i < value.documents.length; i++) {
med.add(value.documents[i].data["Name"]);
}
} else {
print("Error Med Value not found");
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 70, 20, 0),
child: Container(
child: Column(
children: [
Expanded(
child: Container(
color: Colors.white,
height: 400,
width: double.infinity,
child: SingleChildScrollView(
child: Column(
children: containerList,
),
),
),
),
Row(
children: [
Spacer(),
RaisedButton(
child: Text("Add"),
onPressed: () {
setState(() {
containerList.insert(0, new MyDynamicWidget());
});
},
),
Spacer(),
RaisedButton(
child: Text("Remove"),
onPressed: () {
setState(() {
containerList.removeAt(0);
});
},
),
Spacer(),
RaisedButton(
child: Text("Print"),
onPressed: () {
containerList.forEach((element) {
print(element.myController.text.toString());
print(element.mySelectedScale.toString());
});
},
),
Spacer(),
],
)
],
),
),
);
}
}
class MyDynamicWidget extends StatefulWidget {
final TextEditingController myController = new TextEditingController();
String mySelectedScale = med.length > 0 ? med[0] : "Loading";
#override
_MyDynamicWidgetState createState() => _MyDynamicWidgetState();
}
class _MyDynamicWidgetState extends State<MyDynamicWidget> {
#override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: Colors.yellow,
child: Column(
children: [
Container(
height: 50,
child: DropdownButton<String>(
items: med.map((String dropDownStringItem) {
return DropdownMenuItem<String>(
value: dropDownStringItem,
child: Text(dropDownStringItem),
);
}).toList(),
onChanged: (value) {
setState(() {
widget.mySelectedScale = value;
});
},
value: widget.mySelectedScale,
),
),
TextField(
controller: widget.myController,
),
],
),
);
}
}
I implemented two dropdowns. when I select state and district its is working fine. when I change state it is showing me an error. Setting district dropdown value null in getDistricts() method. Please help and thanks in advance.
getting this error in Console:
There should be exactly one item with [DropdownButton]'s value: 1.
Either zero or 2 or more [DropdownMenuItem]s were detected with the
same value 'package:flutter/src/material/dropdown.dart': Failed
assertion: line 828 pos 15: 'items == null || items.isEmpty ||value ==
null || items.where((DropdownMenuItem item) { return item.value
== value; }).length == 1'
class AddAddress extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AddAddressPage();
}
}
class AddAddressPage extends StatefulWidget {
#override
_AddAddressPageState createState() => _AddAddressPageState();
}
class _AddAddressPageState extends State<AddAddressPage> {
bool loader = false;
int intState;
int intDistrict;
List<Location> districts=listDistricts;
List<Location> states=listStates;
getDistricts()async{
setState(() {
loader=false;
});
List<Location> district= await service.getDistrictsByStateId(intState);
setState(() {
districts=district;
loader=false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Address"),
backgroundColor: Colors.white,
centerTitle: true,
),
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 10.0, left: 30, right: 30),
child: Form(
child: Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 15.0),
child: DropdownButtonFormField<int>(
decoration: InputDecoration(
contentPadding:
const EdgeInsets.only(left: 30, right: 10),
border: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(15))),
),
hint: Text('state'),
value: intState,
items: states.map((location) {
return new DropdownMenuItem<int>(
child: Text(location.name),
value: location.id,
);
}).toList(),
onChanged: (int value) {
setState(() {
intState = value;
intDistrict=null;
getDistricts();
});
}),
),
Padding(
padding: const EdgeInsets.only(top: 15.0),
child: DropdownButtonFormField<int>(
decoration: InputDecoration(
contentPadding:
const EdgeInsets.only(left: 30, right: 10),
border: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(15))),
),
hint: Text(' district'),
value: intDistrict,
items: districts.map((location) {
return new DropdownMenuItem<int>(
child: Text(location.name),
value: location.id,
);
}).toList(),
onChanged: (int value) {
intDistrict = value;
}),
),
],
),
),
),
),
),
ProgressLoader(
loader: loader,
)
],
),
);
}
}
I think the problem is that as you select new state you are just setting intDistrict value to null while District dropdown has dropdownitems, so i think if you clear districts before setting intDistrict to null then it should work.
onChanged: (int value) {
setState(() {
intState = value;
districts.clear(); // added line
intDistrict=null;
getDistricts();
});
}),
For anyone struggling with this, you need to set a default value for your district. So that (1) dropdown is never empty, (2) dropdown value will always match at least 1 item's value if districts come from, say API as an empty array.
int intDistrict = 999; /// example
List<Location> district= await service.getDistrictsByStateId(intState);
/// set a default district on getDistricts()
district.add(Location(name = "select district", id: 999));
setState(() {
districts=district;
loader=false;
});
/// when you change to a new state, set intDistrict to default
onChanged: (int value) {
setState(() {
intState = value;
intDistrict = 999;
getDistricts();
});
Thanks for the help guys. I found the issue.since I upgraded the flutter SDK(not Stable SDK) in dropdown.dart file there ares some changes with the previous. So I compared with the stable SDK and modified the file. Thanks for your time and help guys.
I am having a problem with SharedPreferences and multiple Modules I am generating on a Form using ListView.builder
The form is basically asking for some parents details and their childs details - by default the form assumes the parent has one child, but more can be added by clicking a button. The ChildModule has the ability to "close" but when "re-opened" the data doesn't persist, hence using SharedPreferences, it works fine with one Child, but once a second child is added it seems to be creating multiple Instances of SharedPreferences.
I have cut out everything to show what I am trying to achieve. NOTE this is being used as a Web App if it matters.
Oh and ChildModule needs to have its own state because it has a widget which requires it (not shown)
ENQUIRY FORM
final GlobalKey<FormBuilderState> _enquiryFormKey = GlobalKey<FormBuilderState>();
class EnquiryForm extends StatefulWidget {
static List<int> numberOfChildren = [1];
#override
_EnquiryFormState createState() => _EnquiryFormState();
}
class _EnquiryFormState extends State<EnquiryForm> {
int defaultNumberOfChildren = 1;
removeModule(){
setState(() {});
}
#override
Widget build(BuildContext context) {
return FormBuilder(
key: _enquiryFormKey,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: CustomTextField(
label: 'Parent First Name',
isRequired: true,
),
),
),
//ChildModuleList
ListView.builder(
shrinkWrap: true,
itemCount: EnquiryForm.numberOfChildren.length,
itemBuilder: (context,int index){
return ChildModule(EnquiryForm.numberOfChildren[index], removeModule);
}
),
SizedBox(height: 20,),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
ThemedButton(
onPressed: (){
setState(() {
defaultNumberOfChildren++;
EnquiryForm.numberOfChildren.add(defaultNumberOfChildren);
});
},
child: Text(
'Add Additional Child',
style: TextStyle(color: Colors.white),)
),
SizedBox(width: 10,),
ThemedButton(
onPressed: (){},
child: Text('Enquire Now', style: TextStyle(color: Colors.white),))
],
)
],
));
}
}
CHILD MODULE
class ChildModule extends StatefulWidget {
final int number;
final Function() callback;
ChildModule(this.number,this.callback);
#override
_ChildModule createState() => _ChildModule();
}
class _ChildModule extends State<ChildModule> {
SharedPreferences childModuleData;
String firstName;
bool loading = true;
bool isOpen;
#override
void initState() {
print('this module number is ${widget.number}');
_spInstance();
isOpen = true;
super.initState();
}
Future<void> _spInstance() async {
if(childModuleData == null && widget.number == 1) {
childModuleData = await SharedPreferences.getInstance();
print('got instance');
} else {
print('broken');
print(childModuleData);
};
String _testValue = childModuleData.getString('Child First Name');
if(_testValue == null){
childModuleData.setString('Child First Name', '');
loading = false;
} else {
childModuleData.clear();
_spInstance();
}
}
#override
Widget build(BuildContext context) {
return loading ? Loading() : Column(
children: <Widget>[
GestureDetector(
onTap: () {
setState(() {
if (isOpen == false) {
isOpen = true;
} else {
isOpen = false;
}
});
},
child: Container(
height: 40,
padding: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
color: Colors.blue[50],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Child Details',
style: TextStyle(color: Colors.blue[300], fontSize: 16),
),
Row(
children: <Widget>[
isOpen
? Icon(
Icons.arrow_drop_down,
size: 30,
)
: Transform.rotate(
angle: math.pi / 2,
child: Icon(
Icons.arrow_drop_down,
size: 30,
),
),
widget.number > 1
? IconButton(icon: Icon(Icons.clear), onPressed: () async {
await FormFunctions().removeModule(widget.number, EnquiryForm.numberOfChildren);
widget.callback();
})
: Container(),
],
),
],
),
),
),
AnimatedContainer(
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
height: isOpen ? null : 0,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]),
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
CustomTextField(
label: fieldFirstName,
isRequired: true,
initalValue: childModuleData.getString(fieldFirstName) ?? '',
onChanged: (value){
childModuleData.setString(fieldFirstName, value);
},
),
],
),
],
),
),
],
);
}
}
CONSOLE ERROR AFTER SECOND MODULE IS CREATED
Launching lib/main.dart on Chrome in debug mode...
Syncing files to device Chrome...
Debug service listening on ws://127.0.0.1:58490/EkMAy9CGY74=
Debug service listening on ws://127.0.0.1:58490/EkMAy9CGY74=
this module number is 1
got instance
Instance of 'SharedPreferences'
null
this module number is 2 //Number 2 Module is created
broken
null
null
TypeError: Cannot read property 'getString' of null
at child_module._ChildModule.new._spInstance$ (http://localhost:58433/packages/webenrol/shared/widgets/day_button.dart.lib.js:3704:47)
at _spInstance$.next (<anonymous>)
at runBody (http://localhost:58433/dart_sdk.js:43121:34)
at Object._async [as async] (http://localhost:58433/dart_sdk.js:43149:7)
at child_module._ChildModule.new.[_spInstance] (http://localhost:58433/packages/webenrol/shared/widgets/day_button.dart.lib.js:3694:20)
at child_module._ChildModule.new.initState (http://localhost:58433/packages/webenrol/shared/widgets/day_button.dart.lib.js:3689:24)
at framework.StatefulElement.new.[_firstBuild] (http://localhost:58433/packages/flutter/src/widgets/widget_span.dart.lib.js:41219:58)
at framework.StatefulElement.new.mount (http://localhost:58433/packages/flutter/src/widgets/widget_span.dart.lib.js:12605:24)
at framework.SingleChildRenderObjectElement.new.inflateWidget (http://localhost:58433/packages/flutter/src/widgets/widget_span.dart.lib.js:11420:16)
in your case the concern is at the level of recording data in shared preferences. one solution would be to add the widget number in the key to save like this (await SharedPreferences.getInstance()).setString("Your Key${widgetNumber}");
and edit your function _spInstance
class ChildModule extends StatefulWidget {
final int number;
final Function() callback;
ChildModule(this.number,this.callback);
#override
_ChildModule createState() => _ChildModule();
}
class _ChildModule extends State<ChildModule> {
SharedPreferences childModuleData;
...
Future<void> _spInstance() async {
if(childModuleData == null) {
childModuleData = await SharedPreferences.getInstance();
print('got instance');
}
String _testValue = childModuleData.getString('Child First Name${widget.number}');
//NB: do not clear data on chlidModuleData any more.
...
}
...
}