How to detect user has stopped typing in TextField? - flutter

I am using a TextField under which onChanged function
I am calling my code but the issue right now is the code gets execute everytime a new word is either entered or deleted.
what I am looking for is how to recognize when a user has stopped typing.
means adding some delay or something like that.
I have tried adding delay also using Future.delayed function but that function also gets executed n number of times.
TextField(
controller: textController,
onChanged: (val) {
if (textController.text.length > 3) {
Future.delayed(Duration(milliseconds: 450), () {
//code here
});
}
setState(() {});
},
)

Thanks to #pskink
I was able to achieve the functionality I was looking for.
import stream_transform package in your pubspec.yaml
stream_transform: ^0.0.19
import 'package:stream_transform/stream_transform.dart';
StreamController<String> streamController = StreamController();
#override
void initState() {
streamController.stream
.transform(debounce(Duration(milliseconds: 400)))
.listen((s) => _validateValues());
super.initState();
}
//function I am using to perform some logic
_validateValues() {
if (textController.text.length > 3) {
// code here
}else{
// some other code here
}
}
TextField code
TextField(
controller: textController,
onChanged: (val) {
streamController.add(val);
},
)

In my case I also required flutter async.
//pubspec.yaml
stream_transform: ^2.0.0
import 'dart:async';
import 'package:stream_transform/stream_transform.dart';
StreamController<String> streamController = StreamController();
// In init function
streamController.stream
.debounce(Duration(seconds: 1))
.listen((s) => {
// your code
});
// In build function
TextField(
style: TextStyle(fontSize: 16),
controller: messageController,
onChanged: (val) {
myMetaRef.child("isTyping").set(true);
streamController.add(val);
},
)

I find something so lighter in this link
it define this class
class Debouncer {
final int milliseconds;
Timer? _timer;
Debouncer({this.milliseconds=500});
run(VoidCallback action) {
if (null != _timer) {
_timer!.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}
this sample will help you more
TextField(
decoration: new InputDecoration(hintText: 'Search'),
onChanged: (string) {
_debouncer.run(() {
print(string);
//perform search here
});
},
),

Related

Flutter Textformfield search receives data but does not display other data

This is my search code. It works by typing id the codes to display the information.
onSearch(String text) async {
if (text.isNotEmpty) {
List<Item> itemList = [];
for (var item in items) {
if (item.custnum == text.toLowerCase().toUpperCase()) {
itemList.add(item);
}
}
setState(() {
searchitems.clear();
searchitems.addAll(itemList);
print('name : ${searchitems[0].name}');
// if (searchitems.isEmpty) {
// searchitems = [];
// print('searchitems : ${searchitems[0].address!.length}');
// print('searchitems : ${searchitems[0].address!}');
});
} else {
setState(() {
searchCus.clear();
searchitems.clear();
// searchitems.addAll(items);
print('searchitems : $searchitems');
});
}
}
This is my textformfield, it can find and display data. But what I will do is Getting a code from another page It's received and shows the code. But it doesn't show me the information. It has to delete and type at least 1 new password to show the information. Please help me i tried a lot.
TextFormField(
initialValue:
'${searchCus.isEmpty ? "" : searchCus[widget.indexCus].custnum}',
onChanged: onSearch,
decoration: InputDecoration(
labelText: 'custnum',
labelStyle: TextStyle(fontFamily: 'supermarket', fontSize: 14),
isDense: true,
),
),
The reason the initial value not do the search is that you search logic is only works when you type in the textfield, if your initial value come from class constructor you can call onSearch in initState like this:
#override
void initState() {
super.initState();
if(searchCus.isNotEmpty){
onSearch(searchCus[widget.indexCus].custnum);
}
}

Not able to update a list with set state in textfield onchanged method

I want to update a list of type SongModel when I enter a value in the TextField. However, the list is not updating when onChanged is called.
List<SongModel> songs = item.data!;
List<SongModel> filterSongs = [];
//showing the songs
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
keyboardType: TextInputType.text,
controller: searchController,
onChanged: (value) {
//pass value for the search
getSearch(filterSongs,songs);
},
decoration: InputDecoration(............
getSearch() :
getSearch(List<SongModel> filterSongs,List<SongModel> songs)
{
var text = searchController.text;
if (text.isEmpty) {
setState(() {
filterSongs = songs;
});
}
print(songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList());
print(text);
setState(() {
// search = text;
filterSongs = songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList();
});
print(filterSongs.length);
}
Here the list is not updating with set state method.
In your getSearch method, you are setting the value of the parameter passed to getSearch, not the value of the list outside of that method. You should move the filterSongs list declaration outside of your build method anyways so that it isn't redeclared every time the screen is rebuilt.
class MyScreenClassState extends State<MyScreenClass>{
//Create State method here
List<SongModel> filterSongs = [];
//Build method here
}
getSearch(List<SongModel> songs)
{
var text = searchController.text;
if (text.isEmpty) {
setState(() {
filterSongs = songs;
});
}
print(songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList());
print(text);
setState(() {
// search = text;
filterSongs = songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList();
});
print(filterSongs.length);
}
If onChanged method doesn't work for your implementation, you can try this structure I think I will work for you.
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.addListener(_onControllerChange);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
void _onControllerChange() async {
.....
}

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.

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.

Flutter- TextEditingController listener get called multiple time for textfield

TextField controlled controller.addListener(() gets called multiple time after pressing the clear button, this will only happen if we are clearing it.
Snippet:
TextEditingController controller = new TextEditingController();
TextField field = new TextField(
controller: controller,
autofocus: true,
);
controller.addListener(() {
print("Pressed cancel button");
});
Video Link
Note: While adding characters in TextField listener method gets called only ones.
I guess that would be a defect on flutter, a possible solution would be to use onChanged()
TextField field = new TextField(
autofocus: true,
onChanged: (String value) {
print("Pressed clear button");
},
);
I have the same problem with Nexus 6p when used with API level 23 and Pixel with API 25.
but this problem did not occurs with Pixel with API28 and it does not occurs with Nexus6P with API26.
exact code from https://flutter.dev/docs/cookbook/forms/text-field-changes was used.
1. We need to create our own .clear() method
void clearField() {
print("c: clearField");
var newValue = textController.value.copyWith(
text: '',
selection: TextSelection.collapsed(offset: 0),
);
textController.value = newValue;
callApi('');
}
// and call it by :
child: TextField(
controller: textController,
autofocus: true,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: clearField, // call
),
),
),
2. We need to carefully handle changes
void changesOnField() {
print("c: changesOnField");
String text = textController.text;
if (text.isNotEmpty) { // set this
callApi(text);
}
}
Full Code
You may look into this repo and build it locally Github
Result