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

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 🤓

Related

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.

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.

Flutter password validator is never called using StreamTransformer

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 ?

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.

set value in textfield from a list using stream/blocs

I am having problem setting a value to a textfield when using stream/blocs. basically, i have a textfield, when click i want a pop up dialog to come up with a list of value to pick. when user pick a value, that value should be set in the textfield. when using regular textfield i know i can use textfield.text = value but when using stream/blocs is different. i dont know how to use it.
here is my stream code
import 'dart:async';
import 'validators.dart';
import 'package:rxdart/rxdart.dart';
class Bloc extends Object with Validators {
final amountController = BehaviorSubject<String>();
final frequencyController = BehaviorSubject<String>();
final datePaidController = BehaviorSubject<String>();
final categoryController = BehaviorSubject<String>();
final depositToController = BehaviorSubject<String>();
final descriptionController = BehaviorSubject<String>();
// Add data to stream
Stream<String> get amount => amountController.stream.transform(validateAmount);
Stream<String> get frequency =>
frequencyController.stream.transform(validateEmpty);
Stream<String> get datePaid =>
datePaidController.stream.transform(validateEmpty);
Stream<String> get category =>
categoryController.stream.transform(validateEmpty);
Stream<String> get deposit =>
depositToController.stream.transform(validateEmpty);
Stream<String> get description =>
descriptionController.stream.transform(validateEmpty);
/*
Stream<bool> get submitValid =>
Observable.combineLatest6(amount, frequency, datePaid, category, deposit,
description, (a,b,c,d,e,f) => true);
*/
// change data
Function(String) get changeAmount => amountController.sink.add;
Function(String) get changeFrequency => frequencyController.sink.add;
Function(String) get changeDatePaid => datePaidController.sink.add;
Function(String) get changeCategory => categoryController.sink.add;
Function(String) get changeDepositTo => depositToController.sink.add;
Function(String) get changeDescription => descriptionController.sink.add;
submit() {
final validAmount = amountController.value;
final validFrequency = frequencyController.value;
final validDatePaid = datePaidController.value;
final validCategory = categoryController.value;
final validDepositTo = depositToController.value;
final validDescription = descriptionController.value;
// print('Email is $validEmail, and password is $validPassword');
}
dispose() {
amountController.close();
frequencyController.close();
datePaidController.close();
categoryController.close();
depositToController.close();
descriptionController.close();
}
}
my textfield code (different class than the one above)
final bloc = Provider.of(context);
return StreamBuilder(
stream: bloc.amount,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeAmount,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: 'Income Amount',
// labelText: 'Email Address',
errorText: snapshot.error,
prefixIcon: Icon(Icons.attach_money),
suffix: IconButton(
icon: Icon(Icons.arrow_right),
onPressed: () => {
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("Alert Dialog title"),
content: new Text("Alert Dialog body"),
actions: <Widget>[
// usually list of items to choose from
),
],
);
},
)
},
),
),
);
},
);
if you take a look at the textfield code, there is onPressed function for the icon on the textfield. when pressed, a dialog should appear for user to choose a value. once a value is chosen, i want to set the textfield with that value. i am using streams/blocs and i dont know how to do it since the text controller is in Bloc class and the textfield code is another class. can someone help on how to set the textfield value when user choose from a popup list? thanks in advance
Use a TextEditingController.
It will allow you to process and set text values to/from the text field.
You can use it like this:
final _controller = TextEditingController();
In text field, assign the _controller:
TextField(
controller: _controller,
//everything else should be same as before
),
When required to update the text field with new value you can do this:
_controller.text = newAmountFromStream;