Flutter password validator is never called using StreamTransformer - flutter

The issue
I'm using the BLoC pattern to validate TextFields.
For that I'm using StreamController class to sink and listen to the streams.
Although , the Validator is never called.
Classes & Widget
This is my validator class
class Validators {
final validatePassword = StreamTransformer<String,String>.fromHandlers(
handleData: (password,sink)
{
if(password.length > 4)
{
sink.add(password);
}else{
sink.add('Password must be atleast 5 characters');
}
}
);
}
Then I'm building the BLoC class
class Bloc with Validators{
final _passwordController = StreamController<String>();
Function(String) get addPasswordStream => _passwordController.sink.add;
Stream<String> get getPasswordStream => _passwordController.stream.transform(validatePassword);
}
final bloc = Block ()
And adding it to my Cusotm Widget by using the StreamBuilder class
Widget passwordField(){
return StreamBuilder(
stream: bloc.getPasswordStream,
builder: (context,snapshot){
return TextField(
onChanged: bloc.addPasswordStream,
obscureText: true,
decoration: InputDecoration(
hintText: 'password',
labelText: 'password',
errorText: snapshot.error
),
);
},
);
}
I've implemented the same method for the email validator and it works , but for some reason when I'm implementing it to the password TextField as well, it doesn't. I've used flutter clean and flutter run nothing changed. What have i completely missed ?

Related

How to make provider listener listen only once

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.

Flutter test can't found widget on almost exactly same cases

I'm starting to learn flutter test unit, and i use firebase flutter login with bloc as the base. I changed some part of it on my code and the tests always fails. I tried to only changed the suspected code on flutter firebase login source code and the results is exactly the same as on my apps. Here the changed code:
signup_form.dart
...
class _EmailInput extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<SignUpCubit, SignUpState>(
buildWhen: (previous, current) => previous.email != current.email,
builder: (context, state) {
return TextField(
key: const Key('signUpForm_emailInput_textField'),
onChanged: (email) => context.read<SignUpCubit>().emailChanged(email),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: state.email.invalid ? 'Email is invalid' : null,
),
);
},
);
}
}
...
sign_up_form_test.dart
...
testWidgets('invalid email error text when email is invalid',
(tester) async {
final email = MockEmail();
when(() => email.invalid).thenReturn(true);
when(() => signUpCubit.state).thenReturn(SignUpState(email: email));
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BlocProvider.value(
value: signUpCubit,
child: const SignUpForm(),
),
),
),
);
expect(find.text('Email is invalid'), findsOneWidget);
});
...
I changed the errorType so i don't need to bloat my view file (because i want to add more state later, like email registered, email invalid, email empty, and on password i can put like minimum 8 char, need uppercase etc.). I added errorText getter on Email model (this getter is not exist on vanilla flutter firebase login)
email.dart
...
/// Get error message
///
/// Return error in #String
String? get errorText {
String? errorMessage;
if (invalid) {
errorMessage = 'Email is invalid';
}
return errorMessage;
}
...
and so I changed the errorText on Textfield to
signup_form.dart
...
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: state.email.errorText,
),
...
they should act exactly the same right? because state.email.invalid is accessing invalid getter so in email.dart is excatly the same as accessing invalid directly. I even changed the code to like below but the results still same
email.dart
...
String? get errorText => invalid ? 'invalid email' : null;
...
Do I use wrong test code? or I misunderstand how unit test work? I really hope I can se behind the results when testing is running because when I test it myself, I see no difference between them. Even when I only change the errorText value from signup_form.dart back to original, the test unit is success again, (so only errorText: state.email.invalid ? 'Email is invalid' : null, and errorText: state.email.errorText, is difference, where errorText is String? get errorText => invalid ? 'Email is invalid' : null;).
P.S. I don't mind if you ask me to write more code here, I just don't want my questions become too long so I skipped most of the codes related to this. btw I'm using Cubit to detect the change of the state.

Flutter Bloc pattern question from The complete developers guide course Udemy 165

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 🤓

Reusing widgets with Bloc

What do I want to achieve?
I'm using the Flutter BLoc library for my authentication component. Inside login page I have some text fields like username/password that I'm going to share with other pages as well, such as register page, forgot password, change password etc. Basically, following the DRY principle.
Each page (Register, Login, Forgot Password etc.) has it's own BLoc component.
My problem
I couldn't find a way to decouple the widget from the BLoc. I want to be able pass in the stateful widget any BLoc component depending on the page it is used in.
To make sense out of the written above let's take a look at my code.
login.dart A of piece of code from the build method from the Login page.
Widget _loginForm(){
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
final status = state.formStatus;
if (status is SubmissionFailed) {
...
}
},
child: Form(
key: _formKey,
child: Column(
children: [
// The reusable widget: Email input field.
BlocBuilder<LoginBloc, LoginState>(
builder:(context, state){
return emailInputField(context, state);
}
),
...
Now let's take a look in the emailInputField widget
Widget emailInputField(BuildContext context, dynamic state) {
return TextFormField(
validator: (value) =>
state.isValidEmail ? null : 'Please input a valid email',
onChanged: (value) =>
// HERE - I want to decouple this widget from "LoginBloc".
context.read<LoginBloc>().add(LoginUsernameChanged(username: value)),
labelText: 'Email',
...
);
}
login_bloc.dart The Login BLoc
class LoginBloc extends Bloc<BaseEvent, LoginState>{
LoginBloc() : super(LoginState());
#override
Stream<LoginState> mapEventToState(BaseEvent event) async*{
yield* event.handleEvent(state);
}
}
And the login events class, for having a full picture login_event.dart
abstract class LoginEvent extends BaseEvent {
AuthenticationService authService = GetIt.I.get<AuthenticationService>();
}
// Event 1
class LoginUsernameChanged extends LoginEvent {
final String username;
LoginUsernameChanged({this.username});
#override
Stream<LoginState> handleEvent(BaseState state) async* {
// Dart style down-casting...
LoginState loginState = state;
yield loginState.copyWith(username: username);
}
}
base_event.dart
abstract class BaseEvent {
Stream<BaseState> handleEvent(BaseState state);
}
And again, is there a way to decouple that view from the BLoc?
I hope my question makes sense after reading this.
P.S
To make things even simpler, one idea I though of is to keep one single Bloc component that will handle a group of related input fields (password/username) and then just pass in different states as I did in login.dart page when calling the emailInput widget.
What I would do is decouple the widget from bloc entirely. Instead of taking the bloc state and using bloc.add, create dependencies that you can populate with any parameter.
In you example you would have:
Widget emailInputField({
required BuildContext context,
required bool isValidEmail,
required void Function(String?) onChange,
}) {
return TextFormField(
validator: (value) => isValidEmail ? null : 'Please input a valid email',
onChanged: onChange,
labelText: 'Email',
...
);
}
Then you can use emailInputField with any bloc you want. Or any state management library for that matter.
For your example this would give:
Widget _loginForm(){
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
final status = state.formStatus;
if (status is SubmissionFailed) {
...
}
},
child: Form(
key: _formKey,
child: Column(
children: [
// The reusable widget: Email input field.
BlocBuilder<LoginBloc, LoginState>(
builder:(context, state){
return emailInputField(
context: context,
isValidEmail: state.isValidEmail,
onChanged: (value) => context.read<LoginBloc>().add(LoginUsernameChanged(username: value))
);
}
),
...

Error when supplying an initial value to TextField

I am facing a problem with TextField, since I am working hand in hand with TextEditingController()..text and onChanged, but when entering a new data, it is not reflected in the TextField. I made a print inside the onChanged this same one recognizes a new entry but the value to initiate continues without being updated. Inside the onChanged, I have a function which is in charge of validating what is entered and returning an error if necessary. When I comment the instruction before mentioned the TextField already allows to enter and to update what the user enters.
I hope you can help me, a feedback, tutorial, etc.
I would appreciate it.
TextField Code:
CustomTextField(
controller: TextEditingController()..text = datumAdministrative.name,
placeholder: Constants.selectDate,
helperText: Constants.requiredData,
keyboardType: TextInputType.text,
enable: true,
errorText: validationForm.name.error,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
onChanged: (String value) {
validationForm.changeName(value);
},
);
ValidationForm Code:
void changeName(String value) {
String pattern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(pattern);
regExp.hasMatch(value)
? _name = ValidationItem(value, null)
: _name = ValidationItem(null, Constants.nameAdministrativeMessage);
notifyListeners();
}
Try Using : TextController(text: "<Required Text>")
Also does the validation have to be every single time the user enter any word ?
If not, you can try validation everything at the end.
If you are trying to use reactive validation, make sure your CustomTextField is wrapped with the widget which is responsible for rebuilding the UI.. something like Consumer() when using provider package
The problem is probably caused because the TextEditingController object is getting discarded by the rebuilds made by Flutter because you are instantiating the TextEditingController inside a build method. You should save the instance of your controller elsewhere, like in a state object as shown by the official docs or in your case, you can create it and get it from your validationForm.
This is the example in the docs:
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
/// save the [TextEditingController] instance
final TextEditingController _controller = TextEditingController();
...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(6),
child: TextFormField(
controller: _controller, // User your instance
decoration: const InputDecoration(border: OutlineInputBorder()),
),
),
);
}
}
Possible solution in your case:
CustomTextField(
controller: validationForm.myTextController,
placeholder: Constants.selectDate,
...