Check out flutter_form_bloc, it support async validators and you can set the debounce time, in addition to offering other advantages.
You can use TextFieldBloc
without a FormBloc
, but it is much more powerful if you use inside a FormBloc
...
_phoneFieldBloc = TextFieldBloc(
asyncValidatorDebounceTime: Duration(milliseconds: 300),
asyncValidators: [_validatePhone],
);
...
...
TextFieldBlocBuilder(
textFieldBloc: _phoneFieldBloc,
suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
decoration: InputDecoration(labelText: 'Phone number'),
keyboardType: TextInputType.phone,
),
...
.
Example #1 - Without FormBloc

dependencies:
form_bloc: ^0.5.2
flutter_form_bloc: ^0.4.3
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:form_bloc/form_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
void main() => runApp(MaterialApp(home: HomeScreen()));
class HomeScreen extends StatefulWidget {
HomeScreen({Key key}) : super(key: key);
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
TextFieldBloc _phoneFieldBloc;
StreamSubscription<TextFieldBlocState> _textFieldBlocSubscription;
@override
void initState() {
super.initState();
_phoneFieldBloc = TextFieldBloc(
asyncValidatorDebounceTime: Duration(milliseconds: 300),
asyncValidators: [_validatePhone],
);
_textFieldBlocSubscription = _phoneFieldBloc.state.listen((state) {
if (state.isValid) {
// Print the value of the _textFieldBloc when has a valid value
print(state.value);
}
});
}
@override
void dispose() {
_phoneFieldBloc.dispose();
_textFieldBlocSubscription.cancel();
super.dispose();
}
Future<String> _validatePhone(String number) async {
// Fake phone async validator
await Future<void>.delayed(Duration(milliseconds: 200));
if (number.length > 4 && number.length < 9) {
return 'Your phone number is not valid';
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextFieldBlocBuilder(
textFieldBloc: _phoneFieldBloc,
suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
decoration: InputDecoration(labelText: 'Phone number'),
keyboardType: TextInputType.phone,
),
),
);
}
}
Example #2 - With FormBloc

dependencies:
form_bloc: ^0.5.2
flutter_form_bloc: ^0.4.3
import 'package:flutter/material.dart';
import 'package:form_bloc/form_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
void main() => runApp(MaterialApp(home: HomeScreen()));
class SimpleFormBloc extends FormBloc<String, String> {
final phoneField = TextFieldBloc(
asyncValidatorDebounceTime: Duration(milliseconds: 600),
);
final emailField = TextFieldBloc(
validators: [Validators.email],
asyncValidatorDebounceTime: Duration(milliseconds: 300),
);
@override
List<FieldBloc> get fieldBlocs => [phoneField, emailField];
SimpleFormBloc() {
phoneField.addAsyncValidators([_isValidPhone]);
emailField.addAsyncValidators([_isEmailAvailable]);
}
Future<String> _isValidPhone(String number) async {
// Fake phone async validator
await Future<void>.delayed(Duration(milliseconds: 200));
if (number.length > 4 && number.length < 9) {
return 'Your phone number is not valid';
}
return null;
}
Future<String> _isEmailAvailable(String email) async {
// Fake email async validator
await Future<void>.delayed(Duration(milliseconds: 200));
if (email == 'name@domain.com') {
return 'That email is taken. Try another.';
} else {
return null;
}
}
@override
Stream<FormBlocState<String, String>> onSubmitting() async* {
// Form logic...
try {
// Get the fields values:
print(phoneField.value);
print(emailField.value);
await Future<void>.delayed(Duration(seconds: 2));
yield currentState.toSuccess();
} catch (e) {
yield currentState.toFailure(
'Fake error, please continue testing the async validation.');
}
}
}
class HomeScreen extends StatefulWidget {
HomeScreen({Key key}) : super(key: key);
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
SimpleFormBloc _simpleFormBloc;
@override
void initState() {
super.initState();
_simpleFormBloc = SimpleFormBloc();
}
@override
void dispose() {
_simpleFormBloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FormBlocListener(
formBloc: _simpleFormBloc,
onSubmitting: (context, state) {
// Show the progress dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async => false,
child: Center(
child: Card(
child: Container(
width: 80,
height: 80,
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(),
),
),
),
),
);
},
onSuccess: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Navigate to success screen
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SuccessScreen()));
},
onFailure: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Show snackbar with the error
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(state.failureResponse),
backgroundColor: Colors.red[300],
),
);
},
child: ListView(
children: <Widget>[
TextFieldBlocBuilder(
textFieldBloc: _simpleFormBloc.phoneField,
suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
decoration: InputDecoration(labelText: 'Phone number'),
keyboardType: TextInputType.phone,
),
TextFieldBlocBuilder(
textFieldBloc: _simpleFormBloc.emailField,
suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: _simpleFormBloc.submit,
child: Center(child: Text('SUBMIT')),
),
),
],
),
),
);
}
}
class SuccessScreen extends StatelessWidget {
const SuccessScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green[300],
body: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Icon(
Icons.sentiment_satisfied,
size: 100,
),
RaisedButton(
color: Colors.green[100],
child: Text('Go to home'),
onPressed: () => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => HomeScreen())),
)
],
),
),
),
);
}
}