set value in textfield from a list using stream/blocs - flutter

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;

Related

Flutter Multi-select via Autocomplete - preserve user input

I am trying to make essentially a Flutter multi-select input with an auto-complete search box, and I'm attempting to use the Material Autocomplete class with a custom User type to do so. The behavior I am after but having trouble with is for the text input to remain unchanged, even as the user makes selections.
What's making this a bit difficult is the displayStringForOption property. This property only takes the instance of the custom type corresponding to the user's selection, and as far as I can tell, nothing that indicates what the current text input is. This causes the input to be overwritten when the user makes a selection, which I would like to avoid. I don't believe the TextEditingController is available either.
Here's an example of what I have at the moment:
#override
Widget build(BuildContext context) =>
Autocomplete<User>(
fieldViewBuilder: _inviteeSearchInput,
displayStringForOption: (u) => '${u.name} - ${u.email}', // <== actually want this to just remain as the user's input
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return widget.availableUsers.where((user) => !_selectedUsers.contains(user));
}
return widget.availableUsers.where((User u) =>
!_selectedUsers.contains(u) && (u.name.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
u.email.toLowerCase().contains(textEditingValue.text.toLowerCase())));
},
optionsViewBuilder: ...,
onSelected: (u) {
setState(() {
_selectedUsers = { ..._selectedUsers, u };
});
},
);
Widget _inviteeSearchInput(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) => TextFormField(
controller: textEditingController, // <== the TextEditingController is here, but I don't think that helps?
focusNode: focusNode,
decoration: ...
);
One thought I've tried that seems to work, but for some reason doesn't feel right is to keep track of the user input in a variable and update it with the onChanged property of the fieldViewBuilder:
class MultiSelectAutoCompleteState extends State<MultiSelectAutoComplete> {
Set<User> _selectedUsers = {};
String inputVal = ''; // <== var to keep track of user's input
// ...
#override
Widget build(BuildContext context) =>
Autocomplete<User>(
fieldViewBuilder: _inviteeSearchInput,
displayStringForOption: (u) => inputVal, // <== using that as the display string
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return widget.availableUsers.where((user) => !_selectedUsers.contains(user));
}
return widget.availableUsers.where((User u) =>
!_selectedUsers.contains(u) && (u.name.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
u.email.toLowerCase().contains(textEditingValue.text.toLowerCase())));
},
optionsViewBuilder: ...,
onSelected: (u) {
setState(() {
_selectedUsers = { ..._selectedUsers, u };
});
},
);
Widget _inviteeSearchInput(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) => TextFormField(
controller: textEditingController,
onChanged: (s) => inputVal = s, // <== setting the var here
focusNode: focusNode,
decoration: ...
);
I'm not calling setState in the onChanged callback because I'm not sure I want to trigger a rebuild every time the input changes, but maybe I do?
I'm curious if there's a better way to do this, for some reason, this feels icky to me, and I'm also having a hard time reasoning about whether or not I want to call setState in the onChanged callback if I do go with the second option.

How to save value from TextField in Flutter to use it in other places

i am creating very easy app - user will get 4 texfields and he will put there numbers. Then i want to do some math on that numbers.
How is it possible to save that inputs in variables which i could use wherever i want and in relevant moment?
For now i only created possibility to display it on this class where they were created:
my TextFields look like this (i have 4 textfields: e, f, g and h:
var e = '';
TextField(
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (newVal) {
e = newVal;},),
And this is button - when i click it i can see inputs
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('You wrote $d $e $f $g'),
);
},
);
But how to save that inputs to variables outside this class?
You can save the value from onChanged to a state management solution like StatefulWidget or Providers and so on.
TextField(
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (newVal) {
if(newVal != null){
context.read<SignUpBloc>().add(SignUpEvent.mobileChanged(mobile: newVal));
}
},),
This is how we do it in Bloc. You can do the same in any other state management solution or even store it locally in SharedPreferences as well.
First you need to create a TextEditingController inside you class for each field you want to change:
class _MyClassWithDataState extends State<MyClassWithData> {
TextEditingController textController1 = TextEditingController();
TextEditingController textController2 = TextEditingController();
TextEditingController textController3 = TextEditingController();
TextEditingController textController4 = TextEditingController();
}
and define on the other class the data you will need:
class ClassWithReceivedData extends StatefulWidget {
const ClassWithReceivedData({
Key? key,
required this.text1,
required this.text2,
required this.text3,
required this.text4,
}) : super(key: key);
final TextEditingController text1;
final TextEditingController text2;
final TextEditingController text3;
final TextEditingController text4;
#override
State<ClassWithReceivedData> createState() =>
_ClassWithReceivedDataState();
}
then when you navigate to other page you simple pass:
Navigator.push(context, new MaterialPageRoute(builder: (context) => new
ClassWithReceivedData(text1: textController1, text2: textController2,
text3: textController3, text4: textController4);
but if you really need to retrieve data on multiple classes i suggest to create a Controller or a Store class for the data you need an then use an state managment like Provider, Get, Mobx, etc..., to retrieve data whatever you want.

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 🤓

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 ?

How to change TextField text from Store with flutter_flux?

In the flutter_flux example when we commit a new message, the _currentMessage is emptied but the TextField does not reflect that changes.
This is the code in the store:
triggerOnAction(commitCurrentMessageAction, (ChatUser me) {
final ChatMessage message =
new ChatMessage(sender: me, text: _currentMessage);
_messages.add(message);
_currentMessage = '';
});
The view uses a TextEditingController as a controller for the TextField Widget so I understand why it is not updated.
How can we empty the TextField from the Store with flutter_flux?
EDIT: The flutter_flux example has been updated since I posted this answer, and it now correctly discards message in the TextField but in a better way. You should check it out.
I think the correct way would be to move the TextEditingController to the ChatMessageStore, instead of simply keeping the currentMessage in that store. Then you would be able to empty the text field by calling clear() on the TextEditingController.
Generally speaking, the state values which would normally be kept in FooState in vanilla flutter would go into a Store when using flutter_flux. Since you would normally create and keep a TextEditingController in a State, I think it's more natural to keep it in a Store anyways.
The updated ChatMessageStore would look something like this:
class ChatMessageStore extends Store {
ChatMessageStore() {
triggerOnAction(commitCurrentMessageAction, (ChatUser me) {
final ChatMessage message =
new ChatMessage(sender: me, text: currentMessage);
_messages.add(message);
_msgController.clear();
});
}
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _msgController = new TextEditingController();
List<ChatMessage> get messages =>
new List<ChatMessage>.unmodifiable(_messages);
TextEditingController get msgController => _msgController;
String get currentMessage => _msgController.text;
bool get isComposing => currentMessage.isNotEmpty;
}
Note that we no longer need the setCurrentMessageAction, as the TextEditingController would take care of the text value change itself.
Then, the msgController defined in ChatScreen widget could be removed and the _buildTextComposer could be updated accordingly.
Widget _buildTextComposer(BuildContext context, ChatMessageStore messageStore,
ChatUserStore userStore) {
final ValueChanged<String> commitMessage = (String _) {
commitCurrentMessageAction(userStore.me);
};
ThemeData themeData = Theme.of(context);
return new Row(children: <Widget>[
new Flexible(
child: new TextField(
key: const Key("msgField"),
controller: messageStore.msgController,
decoration: const InputDecoration(hintText: 'Enter message'),
onSubmitted: commitMessage)),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed:
messageStore.isComposing ? () => commitMessage(null) : null,
color: messageStore.isComposing
? themeData.accentColor
: themeData.disabledColor))
]);
}
Hope this helps.