Flutter Multi-select via Autocomplete - preserve user input - flutter

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.

Related

How does the onPressed voidcallback change the boolean in my function?

Situation:
I have a checkbox in one place and i am sending the callback etc. up the widget tree to run a setState and run the function applyFilters().
The NeededChecked is also routed up to the checkbox-value.
Question:
What i am struggling to understand is why this works.
Specifically how the onPressed callback is able to set the value of the bool isNeededState to true/false?
Here is the code that is run. The only important part is the passing of the bool isNeededState to the neededCheked.
void neededFilterCalled(bool isNeededState) {
setState(() {
NeededChecked = isNeededState;
applyFilters();
});
}
And here is the checkbox widget:
Widget build(BuildContext context) {
return Checkbox(
value: isNeededChecked,
onChanged: neededFilterCalled,
);
}
Writing
onChanged: neededFilterCalled,
is shorthand for
onChanged: (value) => neededFilterCalled(value),
onChanged provide nullable bool, defined as
required void Function(bool?)? onChanged
You can accept null value and provide false on null case like
void neededFilterCalled(bool? isNeededState) {
setState(() {
isNeededChecked = isNeededState ?? false;
applyFilters();
});
}
return Checkbox(
value: isNeededChecked,
onChanged: neededFilterCalled,
);

Why DropdownButtonFormField Wrapped with FutureBuilder Being Rebuilt with Same Data Appended?

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.

Suspicion of infinite loop using Riverpod & PopupMenuButton

I've racked my brains looking for my error but I can't find it. Looking for assistance.
I'm using Riverpod for dependency injection here. Got some sort of infinite loop running once I navigate to screen with a PopupMenuButton. The issue is related to my design of the menu button because if I remove it all is well. With the button my CPU skyrockets, without, it's completely normal.
I've tried inserting print() statements everywhere looking for repeating code but I'm at a loss for where Ive got a problem...
Can anyone see what is going on here?
Service Class
class AppStateService extends ChangeNotifier {
List<String> saleSources = [];
late String? selectedSaleSource;
Future<void> addSaleSource(String salesPerson) async {
Box<String> salesSourcesBox = await Hive.openBox<String>(kBoxSaleSource + '_' + authorizedLocalUser!.userID);
await salesSourcesBox.add(salesPerson);
saleSources = salesSourcesBox.values.toList();
print('1');
notifyListeners();
}
Future<void> saleSourceLogic() async {
Box<String> salesSourcesBox = await Hive.openBox<String>(kBoxSaleSource + '_' + authorizedLocalUser!.userID);
if (salesSourcesBox.values.toList().isEmpty) await salesSourcesBox.add('Add source');
if (salesSourcesBox.values.contains('Add source') && salesSourcesBox.values.toList().length > 1) await salesSourcesBox.deleteAt(0);
saleSources = salesSourcesBox.values.toList();
print('2');
notifyListeners();
}
Future<void> getSaleSources() async {
Box<String> salesSourcesBox = await Hive.openBox<String>(kBoxSaleSource + '_' + authorizedLocalUser!.userID);
saleSources = salesSourcesBox.values.toList();
print('3');
notifyListeners();
}
void setSaleSource(String source) {
print('4');
selectedSaleSource = source;
notifyListeners();
}
}
Widget
class SalesSourcePulldownMenuWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
context.read(appState).saleSourceLogic();
context.read(appState).getSaleSources();
List<String> listItems = context.read(appState).saleSources;
late List<PopupMenuItem<String>> menuItems;
menuItems = listItems
.map((String value) => PopupMenuItem<String>(
value: value,
child: Text(value),
))
.toList();
return PopupMenuButton<String>(
itemBuilder: (BuildContext contect) => menuItems,
onSelected: (String newValue) {
context.read(appState).setSaleSource(newValue);
});
}
}
Screen Snippet
...
TextField(
controller: salesPerson,
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.words,
autocorrect: false,
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black)),
labelText: 'Name',
suffixIcon: SalesSourcePulldownMenuWidget(),
),
),
...
I just figured out that the cause of the loop-like issue is having my PopupMenuButton inside my text field as a suffix icon. Will need to redesign outside textField.
I do not know any technical specifics as to what could have caused this further than the fact that taking it out of the textField fixes the resource drain.
I'll leave this here in case anyone else encounters this and it may help. Or in case anyone else can explain the reason for resource drain better.

Change Value of TextFormField when leaving focus

I have a TextFormField that, if the user leaves it, should fill with specific Text.
So for example, if the user edits the field and enters "Test", and I don't want to allow the String "Test", then as soon as the field looses focus it should replace the Text with "Sike!". So something like onChanged, but the event being the loss of Focus, not a change of value, which is what I had so far:
TextFormField(
controller: myController,
onChanged: (value) {
if (value == "Test"){
myController.text = "Sike!";
}
},
.............
.............
One way you can do this is like so.
class _DemoState extends State<Demo> {
final node = FocusNode();
final tc = TextEditingController();
#override
void initState() {
node.addListener(() {
if (!node.hasFocus && tc.text == 'Test') {
tc.text = "Sike!";
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: tc,
focusNode: node,
);
}
}

Flutter GetX forms validation

I am looking for an example of how to handle forms and validation in best practice with GetX?
Is there any good example of that or can someone show me an example of how we best can do this?
Here's an example of how you could use GetX's observables to dynamically update form fields & submit button.
I make no claim that this is a best practice. I'm sure there's better ways of accomplishing the same. But it's fun to play around with how GetX can be used to perform validation.
Form + Obx
Two widgets of interest that rebuild based on Observable value changes:
TextFormField
InputDecoration's errorText changes & will rebuild this widget
onChanged: fx.usernameChanged doesn't cause rebuilds. This calls a function in the controller usernameChanged(String val) when form field input changes.
It just updates the username observable with a new value.
Could be written as:
onChanged: (val) => fx.username.value = val
ElevatedButton (a "Submit" button)
onPressed function can change between null and a function
null disables the button (only way to do so in Flutter)
a function here will enable the button
class FormObxPage extends StatelessWidget {
const FormObxPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
FormX fx = Get.put(FormX()); // controller
return Scaffold(
appBar: AppBar(
title: const Text('Form Validation'),
),
body: SafeArea(
child: Container(
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(
() {
print('rebuild TextFormField ${fx.errorText.value}');
return TextFormField(
onChanged: fx.usernameChanged, // controller func
decoration: InputDecoration(
labelText: 'Username',
errorText: fx.errorText.value // obs
)
);
},
),
Obx(
() => ElevatedButton(
child: const Text('Submit'),
onPressed: fx.submitFunc.value, // obs
),
)
],
),
),
),
);
}
}
GetX Controller
Explanation / breakdown below
class FormX extends GetxController {
RxString username = RxString('');
RxnString errorText = RxnString(null);
Rxn<Function()> submitFunc = Rxn<Function()>(null);
#override
void onInit() {
super.onInit();
debounce<String>(username, validations, time: const Duration(milliseconds: 500));
}
void validations(String val) async {
errorText.value = null; // reset validation errors to nothing
submitFunc.value = null; // disable submit while validating
if (val.isNotEmpty) {
if (lengthOK(val) && await available(val)) {
print('All validations passed, enable submit btn...');
submitFunc.value = submitFunction();
errorText.value = null;
}
}
}
bool lengthOK(String val, {int minLen = 5}) {
if (val.length < minLen) {
errorText.value = 'min. 5 chars';
return false;
}
return true;
}
Future<bool> available(String val) async {
print('Query availability of: $val');
await Future.delayed(
const Duration(seconds: 1),
() => print('Available query returned')
);
if (val == "Sylvester") {
errorText.value = 'Name Taken';
return false;
}
return true;
}
void usernameChanged(String val) {
username.value = val;
}
Future<bool> Function() submitFunction() {
return () async {
print('Make database call to create ${username.value} account');
await Future.delayed(const Duration(seconds: 1), () => print('User account created'));
return true;
};
}
}
Observables
Starting with the three observables...
RxString username = RxString('');
RxnString errorText = RxnString(null);
Rxn<Function()> submitFunc = Rxn<Function()>(null);
username will hold whatever was last input into the TextFormField.
errorText is instantiated with null initial value so the username field is not "invalid" to begin with. If not null (even empty string), TextFormField will be rendered red to signify invalid input. When a non-valid input is in the field, we'll show an error message. (min. 5 chars in example:)
submitFunc is an observable for holding a submit button function or null, since functions in Dart are actually objects, this is fine. The null value initial assignment will disable the button.
onInit
The debounce worker calls the validations function 500ms after changes to the username observable end.
validations will receive username.value as its argument.
More on workers.
Validations
Inside validations function we put any types of validation we want to run: minimum length, bad characters, name already taken, names we personally dislike due to childhood bullies, etc.
For added realism, the available() function is async. Commonly this would query a database to check username availability so in this example, there's a fake 1 second delay before returning this validation check.
submitFunction() returns a function which will replace the null value in submitFunc observable when we're satisfied the form has valid inputs and we allow the user to proceed.
A little more realistic, we'd prob. expect some return value from the submit button function, so we could have the button function return a future bool:
Future<bool> Function() submitFunction() {
return () async {
print('Make database call to create ${username.value} account');
await Future.delayed(Duration(seconds: 1), () => print('User account created'));
return true;
};
}
GetX is not the solution for everything but it has some few utility methods which can help you achieve what you want. For example you can use a validator along with SnackBar for final check. Here is a code snippet that might help you understand the basics.
TextFormField(
controller: emailController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (!GetUtils.isEmail(value))
return "Email is not valid";
else
return null;
},
),
GetUtils has few handy methods for quick validations and you will have to explore each method to see if it fits your need.