I have created a Flutter stateless dropdown widget that is dependent on some future list, I used the FutureBuilder to build the dropdown as soon as the future is resolved.
But I noticed that build method was being called at least twice. I know it is normal that the build method can be called multiple times when some state changes, but why was the dropdown was being rebuilt with the same data as the previous build call? I thought for sure when build is called, Flutter will rebuild the entire widget which also implies that the previous data will be destroyed as well.
This has resulted in duplication in the items of the dropdown.
I am not sure why it is happening. What did I miss?
class _PetTypeInput extends StatelessWidget {
#override
Widget build(BuildContext context) {
final petTypes = context.read<RegisterPetProfileCubit>().getPetTypes();
return FutureBuilder<List<PetType>>(
future: petTypes,
builder: (BuildContext context, AsyncSnapshot<List<PetType>> snapshot) {
List<PetType>? petKinds = [];
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
petKinds = snapshot.data;
return DropdownButtonFormField<String>(
key: const Key('registerForm_petKindInput_dropdownButtonFormField'),
decoration: const InputDecoration(
labelText: 'pet kind',
helperText: '',
errorText: null,
),
value: 'Dog',
onChanged: (petKindValue) => context
.read<RegisterPetProfileCubit>()
.petKindChanged(petKindValue!),
items: _buildItems(petKinds),
);
}
return const TextField(
enabled: false,
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: 'pet kind',
helperText: '',
),
);
},
);
}
List<DropdownMenuItem<String>> _buildItems(List<PetType>? petKinds) {
final petTypes = petKinds!.fold(
<String, String>{},
(Map<String, String> petTypesMap, petType) {
petTypesMap[petType.id] = petType.label;
return petTypesMap;
},
);
List<String> items = petTypes.keys.toList();
return items.map((key) {
return DropdownMenuItem<String>(
key: Key(key),
child: Text(petTypes[key]!),
value: key,
);
}).toList();
}
}
I can definitely tell that there are no duplicates in the data.
How do I prevent appending the same data? Or clear the previous data of DropdownButtonFormField?
You can build _buildItems(petKinds), before return DropdownButtonFormField<String>( and passing item[0] value on DropdownButtonFormField value. And it will make sure the value contain on DropDownMenuItems. Also, make sure to have Unique value on each DropDownMenuItem.
I finally figured it out. The issue is not duplicate dropdown items but rather my initial value is not a valid value. My initial value was Dog when it should be the id of the Dog item.
So I grabed the first item object and then grabed the id of that item and supplied it as initial value.
The answers from this question helped me figured it out.
Related
I am trying to make essentially a Flutter multi-select input with an auto-complete search box, and I'm attempting to use the Material Autocomplete class with a custom User type to do so. The behavior I am after but having trouble with is for the text input to remain unchanged, even as the user makes selections.
What's making this a bit difficult is the displayStringForOption property. This property only takes the instance of the custom type corresponding to the user's selection, and as far as I can tell, nothing that indicates what the current text input is. This causes the input to be overwritten when the user makes a selection, which I would like to avoid. I don't believe the TextEditingController is available either.
Here's an example of what I have at the moment:
#override
Widget build(BuildContext context) =>
Autocomplete<User>(
fieldViewBuilder: _inviteeSearchInput,
displayStringForOption: (u) => '${u.name} - ${u.email}', // <== actually want this to just remain as the user's input
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return widget.availableUsers.where((user) => !_selectedUsers.contains(user));
}
return widget.availableUsers.where((User u) =>
!_selectedUsers.contains(u) && (u.name.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
u.email.toLowerCase().contains(textEditingValue.text.toLowerCase())));
},
optionsViewBuilder: ...,
onSelected: (u) {
setState(() {
_selectedUsers = { ..._selectedUsers, u };
});
},
);
Widget _inviteeSearchInput(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) => TextFormField(
controller: textEditingController, // <== the TextEditingController is here, but I don't think that helps?
focusNode: focusNode,
decoration: ...
);
One thought I've tried that seems to work, but for some reason doesn't feel right is to keep track of the user input in a variable and update it with the onChanged property of the fieldViewBuilder:
class MultiSelectAutoCompleteState extends State<MultiSelectAutoComplete> {
Set<User> _selectedUsers = {};
String inputVal = ''; // <== var to keep track of user's input
// ...
#override
Widget build(BuildContext context) =>
Autocomplete<User>(
fieldViewBuilder: _inviteeSearchInput,
displayStringForOption: (u) => inputVal, // <== using that as the display string
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return widget.availableUsers.where((user) => !_selectedUsers.contains(user));
}
return widget.availableUsers.where((User u) =>
!_selectedUsers.contains(u) && (u.name.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
u.email.toLowerCase().contains(textEditingValue.text.toLowerCase())));
},
optionsViewBuilder: ...,
onSelected: (u) {
setState(() {
_selectedUsers = { ..._selectedUsers, u };
});
},
);
Widget _inviteeSearchInput(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) => TextFormField(
controller: textEditingController,
onChanged: (s) => inputVal = s, // <== setting the var here
focusNode: focusNode,
decoration: ...
);
I'm not calling setState in the onChanged callback because I'm not sure I want to trigger a rebuild every time the input changes, but maybe I do?
I'm curious if there's a better way to do this, for some reason, this feels icky to me, and I'm also having a hard time reasoning about whether or not I want to call setState in the onChanged callback if I do go with the second option.
I am geting input(text) using textfield and displaying in a list, I am using onchanged property of textfield and provider to update the text of new element in list but, all elements in the list update to onChanged's new value, once the element is added to list I want it to stop listening to changes of onChanged. So, that I can display list with different elements. How do I achieve that.
TextField(
autofocus: true,
decoration: kTextFieldDecocation.copyWith(
hintText: 'B Name'),
onChanged: (newbName) {
Provider.of<BNameControllerClass>(context,
listen: false)
.newBorrowerName(newbName);
},
),
List element's text
Text(
Provider.of<BNameControllerClass>(context,
listen: true)
.bName,
style: const TextStyle(fontSize: 20),
);
provider class
class BNameControllerClass extends ChangeNotifier {
String bName = 'Ganesh';
newBorrowerName(String newName) {
bName = newName;
notifyListeners();
}
}
Create List<String> textsFromInput = [];
Generate text widgets with ListView.generate().
Then in TextField you can use onSubmitted: (value) => textsFromInput.add(value)
Depending on what state management you have you can then call setState() or handle list rebuilding with bloc builder buildWhen: previous.textsFromInput.length != current.textsFromInput.length or with provider.
When I change the "Ulke" value from the AsyncSelectInputForm, I call the notifyListeners() method so that the "Il" value is null.
When I do this, the value I entered in the "Adres Başlığı" TextInputForm returns to its initial value.
My widget:
#override
Widget build(BuildContext context) {
var cariAdres = Provider.of<CariAdresProvider>(context);
return Column(
children: [
TextInputForm(
initialValue: cariAdres.cariAdres?.adresBasligi,
label: "Adres Başlığı",
onChanged: (value) {
cariAdres.cariAdresTemp =
cariAdres.cariAdresTemp?.copyWith(
adresBasligi: value,
);
},
),
//todo ulke select
AsyncSelectInputForm(
pageTitle: "Ülke Seç",
label: "Ülke",
initialLabel: cariAdres.cariAdresTemp?.ulkeIdStr,
labelSetter: (item) {
return item.ulkeStr;
},
onChanged: (value, item, context) {
cariAdres.cariAdresTemp =
cariAdres.cariAdresTemp?.copyWith(
ulkeId: value,
ulkeIdStr: item.ulkeStr,
ilId: null,
ilIdStr: null,
);
cariAdres.notifyListeners();
},
fetchPage: //...,
),
//todo il select
AsyncSelectInputForm(
initialValue: cariAdres.cariAdresTemp?.ilId,
//... same code
)
//....
It can be related a lot of possibilities, so we can't be sure which one is correct. But you can try to add some debugPrint in your build method in this way, you can expand your understanding for the situation.
Also, it can be about some logic in your change notifier provider or it can be about your widget tree-state or it can be about your sub widgets.
My problem was that I had set the initialValue of TextInputForm to value cariAdres.cariAdres?.adresBasligi and valued cariAdres.cariAdresTemp?.ulkeIdStr to AsyncSelectInputForm's initialLabel.
I was able to fix this problem by replacing the cariAdres.cariAdres?.adresBasligi value with the cariAdres.cariAdresTemp?.adresBasligi value. :)
I have a question about this course in Udemy where Im trying to set up Bloc pattern for authentication in Flutter. The problem is that I get a null value from start and also later when I type a required '#' from the snapshot.error of the StreamBuilder. It's set up so I should get an error message until I type in a '#' and then the message should go away. I have three relevant files: loginscreen, bloc and validators. What do you guys think can go wrong here?
Loginscreen:
class LoginScreen extends StatelessWidget {
#override
Widget build(context) {
return Container(
margin: EdgeInsets.all(20.0),
child: Column(
children: [
emailField(),
passwordField(),
//const SizedBox(height: 25.0),
Container(margin: const EdgeInsets.only(top: 25.0)),
submitButton(),
],
),
);
}
Widget emailField() {
//Listener for streams rebuilds builder function when found
return StreamBuilder(
stream: bloc.email,
//snapshot contains the value from the stream
builder: (context, AsyncSnapshot<String>? snapshot) {
return TextField(
onChanged: (newValue) {
print(newValue);
bloc.changeEmail(newValue);
},
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
//This is null for some reason. Something is not right here
//First it shows error because no # and when pressed I get null
errorText: snapshot!.error.toString(),
),
);
});
}
Bloc:
class Bloc with Validators {
//We are working with Strings in the Streamcontrollers (marked with String type)
final _email = StreamController<String>();
final _password = StreamController<String>();
//Annotation with generic type in front. Not required though
// Add data to stream (access to stream)
// Apply the validation transform created
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
// Change data (access to sink)
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
dispose() {
_email.close();
_password.close();
}
}
final bloc = Bloc();
Validators:
class Validators {
final validateEmail =
StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
if (email.contains('#')) {
sink.add(email);
} else {
sink.addError('Enter a valid email!');
}
});
final validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
if (password.length > 7) {
sink.add(password);
} else {
sink.addError('You must be at least 8 characters!');
}
});
}
After change to:
errorText: snapshot?.error?.toString() ?? snapshot?.data,
You are always reading the stream's error here:
errorText: snapshot!.error.toString(),
but you only add errors in this else:
if (email.contains('#')) {
sink.add(email);
} else {
sink.addError('Enter a valid email!');
}
You may try to replace how this value is read by:
//This is null for some reason. Something is not right here
//First it shows error because no # and when pressed I get null
errorText: snapshot?.error?.toString() ?? snapshot?.data,
In this way, if there is no error, errorText will get stream's data.
Update:
If you want to make this message completely go alway after an # be inserted, you may first take a look on how errorText works. From Flutter`s InptuDecorator source code:
/// Text that appears below the [InputDecorator.child] and the border.
///
/// If non-null, the border's color animates to red and the [helperText] is
/// not shown.
///
/// In a [TextFormField], this is overridden by the value returned from
/// [TextFormField.validator], if that is not null.
final String? errorText;
So, this message will be hidden if you not pass a String to it. What is happening in this code here:
errorText: snapshot!.error.toString(),
Is something like this: null.toString();. Dart can and will parse null to string and it will be transformed in a string having null as content.
To fix this, we add an ? mark after the error, like this: snapshot?.error?.toString(),. Now, toString() will only be invoked if error is not null.
Tip: do explore the source code of the widgets that you are using. They are widely commented and you can get information much faster than google random things 🤓
I added two item in list, then add the list to sink.
var list = new List<String>();
AbcBloc _abcBloc = AbcBloc(repository: null);
#override
void didChangeDependencies() {
_abcBloc = Provider.of<AbcBloc>(context);
super.didChangeDependencies();
}
#override
void initState() {
super.initState();
list.add('Electrical');
list.add('Inspection');
_abcBloc.listSink.add(list);
}
But when I try to print the list in TextField, it prints empty?
StreamBuilder(
stream: _abcBloc.listStream,
builder: (context, snapshot) {
return TextField(
decoration: InputDecoration(
labelText: snapshot.data,
....
);
},
),
AbcBloc.dart
final _list = BehaviorSubject<List<String>>();
get listSink => _list.sink;
get listStream => _list.stream;
Edit
After use the suggestion answers, it print null value. Why would this happened?
Firstly, snapshot.data is a list of String items so to display the list as a single String you need to add a toString(); Secondly, you need to be more specific with types - you need to specify what data your StreamBuilder expects, i.e.
StreamBuilder<List<String>>(
stream: _abcBloc.listStream,
builder: (context, snapshot) {
return TextField(
decoration: InputDecoration(
labelText: snapshot.data.toString(),
));
},
),
Taking your example and making those two changes then resulted in [Electrical, Inspection] showing in the text field on my device.
Problems I see:
StreamBuilder has no generic type. Make it StreamBuilder<List<String>>
You're passing List to labelText parameter. You should pass String. (e.g. snapshot.data.toString())
You don't check whether your stream has data or not inside builder. You can do it using snapshot.hasData condition or set initialData parameter.
I was able to fix it.What I did was put the list and the stream in didChangeDependencies.
#override
void didChangeDependencies() {
_abcBloc = Provider.of<AbcBloc>(context);
list.add('Electrical');
list.add('Inspection');
_abcBloc.listSink.add(list);
super.didChangeDependencies();
}