I want to create a TextField that check if the value exist in database.
How to do async validation using BLOC pattern with TextField
widget?
Should I use StreamTransformer
to add error to the Stream
? I tried using DebounceStreamTransformer
but it's just block the Stream
from receiving a new value.
This is my Observable
Observable<String> get valueStream => valueController.stream.transform(PropertyNameExist.handle('Blabla', null));
This is my StreamTransformer
class PropertyNameExist implements StreamTransformerValidator {
static StreamTransformer<String, String> handle(String fieldname, String data) {
Http http = new Http();
return StreamTransformer<String, String>.fromHandlers(
handleData: (String stringData, sink) {
http.post('/my_api',data:{
'property_name':stringData,
}).then((Response response){
Map<String,dynamic> responseData = jsonDecode(response.data);
bool isValid = responseData['valid'] == 'true';
if(isValid){
sink.add(stringData);
} else {
sink.addError('Opps Error');
}
});
});
}
}
This is my Widget
StreamBuilder<String>(
stream: valueStream,
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
_textInputController.setTextAndPosition(snapshot.data);
}
return TextField(
controller: _textInputController,
onChanged: (String newVal) {
updateValue(newVal);
},
decoration: InputDecoration(
errorText: snapshot.error,
),
);
},
)
In this example, learn how to add validation to a form that has a single text field using the following steps: Create a Form with a GlobalKey . Add a TextFormField with validation logic. Create a button to validate and submit the form.
You are probably not longer looking for a solution but based on the upvotes of the question I wanted to provide an answer nonetheless.
I'm not sure if I understand your code correctly and it looks like you are implementing BLoC yourself, so this is quite a disclaimer because I am providing a solution which uses the BLoC implementation by Felix Angelov (pub.dev/packages/bloc).
The outcome of the code described below
Code and Approach:
First I created an empty project, and added the BLoC Library; In pubspec.yaml
i added
flutter_bloc: ^3.2.0
Then i created a new bloc BackendValidationBloc
with one event ValidateInput
and multiple states as shown in the following code snippets.
Event Code:
Most of the time I start by defining the event which is quite simple in my example:
part of 'backend_validation_bloc.dart';
@immutable
abstract class BackendValidationEvent {}
class ValidateInput extends BackendValidationEvent {
final String input;
ValidateInput({@required this.input});
}
State Code:
Then you probably want one state with multiple properties or multiple states. I decided to go with one state with multiple properties because in my opinion it is easier to handle in the UI. In this example I recommend giving feedback to the user because validating the input via a backend might take some time. Therefore the BackendValidationState
features two states: loading
and validated
.
part of 'backend_validation_bloc.dart';
@immutable
class BackendValidationState {
final bool isInProcess;
final bool isValidated;
bool get isError => errorMessage.isNotEmpty;
final String errorMessage;
BackendValidationState(
{this.isInProcess, this.isValidated, this.errorMessage});
factory BackendValidationState.empty() {
return BackendValidationState(
isInProcess: false, isValidated: false);
}
BackendValidationState copyWith(
{bool isInProcess, bool isValidated, String errorMessage}) {
return BackendValidationState(
isValidated: isValidated ?? this.isValidated,
isInProcess: isInProcess ?? this.isInProcess,
// This is intentionally not defined as
// errorMessage: errorMessage ?? this.errorMessage
// because if the errorMessage is null, it means the input was valid
errorMessage: errorMessage,
);
}
BackendValidationState loading() {
return this.copyWith(isInProcess: true);
}
BackendValidationState validated({@required String errorMessage}) {
return this.copyWith(errorMessage: errorMessage, isInProcess: false);
}
}
Bloc Code:
At last, you "connect" event with states by defining the bloc which makes calls to your backend:
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';
part 'backend_validation_event.dart';
part 'backend_validation_state.dart';
class BackendValidationBloc
extends Bloc<BackendValidationEvent, BackendValidationState> {
@override
BackendValidationState get initialState => BackendValidationState.empty();
@override
Stream<BackendValidationState> mapEventToState(
BackendValidationEvent event,
) async* {
if (event is ValidateInput) {
yield this.state.loading();
String backendValidationMessage =
await this.simulatedBackendFunctionality(event.input);
yield this.state.validated(errorMessage: backendValidationMessage);
}
}
Future<String> simulatedBackendFunctionality(String input) async {
// This simulates delay of the backend call
await Future.delayed(Duration(milliseconds: 500));
// This simulates the return of the backend call
String backendValidationMessage;
if (input != 'hello') {
backendValidationMessage = "Input does not equal to 'hello'";
}
return backendValidationMessage;
}
}
UI Code:
In case you are not familiar with how the implemented BLoC is used in the UI, this is the frontend code using the state to feed different values (for the actual error message and for user feedback while waiting for the backend response) to the errorText property of the TextField
.
import 'package:backend_validation_using_bloc/bloc/backend_validation_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider<BackendValidationBloc>(
create: (context) => BackendValidationBloc(), child: HomeScreen()),
);
}
}
class HomeScreen extends StatefulWidget {
HomeScreen({Key key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
TextEditingController textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: BlocBuilder<BackendValidationBloc, BackendValidationState>(
builder: (BuildContext context, BackendValidationState state) {
return TextField(
controller: textEditingController,
onChanged: (String currentValue) {
BlocProvider.of<BackendValidationBloc>(context)
.add(ValidateInput(input: currentValue));
},
decoration: InputDecoration(errorText: state.isInProcess ? 'Valiating input...' : state.errorMessage),
);
},
));
}
}
Connecting a real backend
So I kinda faked a backend, but if you want to use a real one it is common to implement a Repository
and pass it to the BLoC
in a constructor, which makes using different implementations of backend easier (if properly implemented against interfaces). If you want a more detailed tutorial check out Felix Angelov's tutorials (they are pretty good)
Hope this helps you or others.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With