I have a TextField rendered with the help of a StreamBuilder, following the BLoC pattern with a sink and stream.
Widget field(SignUpBloc signUpBloc) {
return StreamBuilder(
stream: signUpBloc.outFirstName,
builder: (context, snapshot) {
return TextField(
style: TextStyle(fontSize: 15.0),
onChanged: signUpBloc.inFirstName,
decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 15.0),
errorText: snapshot.error
),
);
},
);
}
My question is how do I set up an initial value? I've tried with the StreamBuilder's initialData property but no text appears in the TextField.
TextEditingController _controller = TextEditingController(); // make a controller,
Widget field(SignUpBloc signUpBloc) {
return StreamBuilder(
stream: signUpBloc.outFirstName,
initialData: YourData, // provide initial data
builder: (context, snapshot) {
_controller.value = TextEditingValue(text: "${snapshot.data}"); // assign value to controller this way
return TextField(
controller: _controller,
style: TextStyle(fontSize: 15.0),
onChanged: signUpBloc.inFirstName,
decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 15.0),
errorText: snapshot.error
),
);
},
);
}
Edit: To put the cursor at the end of the line, you can use
var cursorPos = _controller.selection;
if (cursorPos.start > _controller.text.length) {
cursorPos = TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
}
_controller.selection = cursorPos;
Source
You will have to have a TextEditingController when you need to pass initial values. Using the value.copyWith strategy there is no need to deal with the cursor, and to make the widget cleaner you can pass the text controller as a parameter.
// Stream widget
Widget field(SignUpBloc signUpBloc, TextEditingController _txtController) {
return StreamBuilder(
stream: signUpBloc.outFirstName,
builder: (context, snapshot) {
_txtController.value =
_txtController.value.copyWith(text: snapshot.data);
return TextField(
controller: _txtController,
onChanged: signUpBloc.inFirstName;
decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 15.0),
errorText: snapshot.error
),
});
}
Related
I was able to get initialValue to work in AutoComplete. But there is a bug where the drop down goes off screen. So I found a workaround on slack and I am not using RawAutoComplete and trying to get the initial Value to work. I tried to set it in RawAutoComplete with:
child: RawAutocomplete<String>(
initialValue: TextEditingValue(text: itemTypeController.text),
When I look at the documentation I see:
This parameter is ignored if [textEditingController] is defined
But I am not sure how to set it otherwise.
I initially tried to set it in the TextFormField like so:
child: TextFormField(
controller: itemTypeController,
initialValue: "test",
focusNode: focusNode,
onEditingComplete: onEditingComplete,
decoration: const InputDecoration(
labelText: "Item type*",
hintText: 'What is the item?',
),
),
But that throws this error:
'initialValue == null || controller == null': is not true.
Which I assume is because if controller is present it woudl take the initial value from there. If both are not null then it doesnt know what to pic. I need the controller because I need to retrieve the value in the form to submit to my database.
Full code below:
LayoutBuilder(
builder: (context, constraints) => InputDecorator(
decoration: const InputDecoration(
icon: Icon(Icons.style),
border: InputBorder.none,
),
child: RawAutocomplete<String>(
initialValue: TextEditingValue(text: itemTypeController.text),
// first property
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return itemTypeList;
}
return itemTypeList.where((String option) {
return option
.toLowerCase()
.contains(textEditingValue.text.toLowerCase());
});
},
//second property where you can limit the overlay pop up suggestion
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: SizedBox(
height: 200.0,
// set width based on you need
width: constraints.biggest.width * 0.8,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final String option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option),
),
);
},
),
),
),
);
},
// third property
fieldViewBuilder:
(context, controller, focusNode, onEditingComplete) {
itemTypeController = controller;
return Focus(
onFocusChange: (hasFocus) {
if (temperatureItemTypes
.contains(itemTypeController.text.trim())) {
//show temperature field
setState(() {
temperatureField = true;
});
} else {
setState(() {
temperatureField = false;
});
}
if (volumeItemTypes
.contains(itemTypeController.text.trim())) {
//show temperature field
setState(() {
volumeField = true;
});
} else {
setState(() {
volumeField = false;
});
}
},
child: TextFormField(
controller: itemTypeController,
focusNode: focusNode,
onEditingComplete: onEditingComplete,
decoration: const InputDecoration(
labelText: "Item type*",
hintText: 'What is the item?',
),
),
);
}),
),
);
I am building a search bar which brings some resuts from algolia. everything is working fine, except that the results listview view doesn't show the new resuts until I close the keyboard. but I need to update automatically with every new letter I write in the text field (while the keyboard is opened), same like auto complete function. what is mimssing here?
(note that all of this is inside a buttomsheet)
I also tried to replace the controller listner with onChange (){}, same issue is there.
the list view doesn't rebuild untill I close the keyboard.
The Funcion and the listner Code:
class _CategoriesPageState extends State<CategoriesPage> {
String _searchTerm = "";
List<AlgoliaObjectSnapshot> _results = [];
bool _searching = false;
TextEditingController _searchText = TextEditingController(text: "");
_search() async {
setState(() {
_searching = true;
});
Algolia algolia = const Algolia.init(
applicationId: 'XP6QXPHMDJ',
apiKey: '283351eb9d0a111a8fb4f2fdb7b8450a',
);
AlgoliaQuery query = algolia.instance.index('BusinessProfilesCollection');
query = query.query(_searchText.text);
_results = (await query.getObjects()).hits;
setState(() {
_searching = false;
});
}
#override
void initState() {
_searchText.addListener(() {
setState(() {
_search();
});
});
super.initState();
}
the Text Field Code:
TextField(
controller: _searchText,
style: GoogleFonts.lato(
fontStyle: FontStyle.normal,
color: Colors.grey[850],
fontSize: 14.sp,
),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Search ...',
hintStyle:
TextStyle(color: Colors.black),
)),
The Results Widget:
Container(
height: 300.h,
child: _searching == true
? Center(
child: Text("Searching, please wait..."),
)
: _results.length == 0
? Center(
child: Text("No results found."),
)
: ListView.builder(
itemCount: _results.length,
itemBuilder:
(BuildContext ctx, int index) {return ...}))
A bottom sheet doesnt update state by default. Wrap the content of the bottom sheet with a statefulbuilder
showModalBottomSheet(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState /*You can rename this!*/) {
return Container(
child: TextField(onChanged: (val) {
setState(() {
//This will rebuild the bottom sheet
});
}),
);
});
});
TextField(
onChanged: (value) {
setState(() {
//This will refresh the page
});
},
controller: _searchText,
style: GoogleFonts.lato(
fontStyle: FontStyle.normal,
color: Colors.grey[850],
fontSize: 14.sp,
),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Search ...',
hintStyle:
TextStyle(color: Colors.black),
),
),
I use a futureBuilder to display date inside TextFormFields, if there is data in the webservice I call in the futureBuilder for the date I selected in the DateTimePicker, the TextFormField is disabled and the data is displayed in it. Else, the textFormField is enabled.
I also have a button that I want to disable if there is data received and enable if there isn't, so I used a boolean.
Here is my code :
child: FutureBuilder<double?>(
future: getTimes(selectedDate),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData){
_timeController.clear();
setState(() {
_isButtonDisabled = false;
});
return TextFormField(
controller: _timeController,
textAlign: TextAlign.center,
enabled: false,
decoration: InputDecoration(
hintText: snapshot.data.toString() + " h",
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: Colors.white70
),
);
}
else {
setState(() {
_isButtonDisabled = true;
});
return TextFormField(
controller: _timeController,
textAlign: TextAlign.center,
enabled: true,
decoration: InputDecoration(
hintText: "0 h",
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: Colors.white
),
);
}
}
)
This was causing me the error setState() or markNeedsBuild called during build , so thanks to the answers of this topic I encapsulated the setState method in WidgetsBinding.instance.addPostFrameCallback((_)
Here is what my code looks like now :
child: FutureBuilder<double?>(
future: getTimes(selectedDate),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData){
_timeController.clear();
WidgetsBinding.instance?.addPostFrameCallback((_){
setState(() {
_isButtonDisabled = false;
});
});
return TextFormField(
controller: _timeController,
textAlign: TextAlign.center,
enabled: false,
decoration: InputDecoration(
hintText: snapshot.data.toString() + " h",
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: Colors.white70
),
);
}
else {
WidgetsBinding.instance?.addPostFrameCallback((_){
setState(() {
_isButtonDisabled = true;
});
});
return TextFormField(
controller: _timeController,
textAlign: TextAlign.center,
enabled: true,
decoration: InputDecoration(
hintText: "0 h",
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: Colors.white
),
);
}
}
)
The problem that I have now is my TextFormFields aren't clickable anymore, and the button is always enabled, may be a misused / misunderstood the addPostFrameCallback function.
Thanks for helping,
You have DateTimePicker, after the selecting date-time you can call the future.
getTimes() returns nullable double. Before retuning data, compare value is null or not and set _isButtonDisabled based on it, assign true/false.
bool _isButtonDisabled = true; // set the intial/watting state you want
Future<double?> getTimes(DateTime time) async {
//heavy operations
return await Future.delayed(Duration(seconds: 3), () {
return 4; //check with null +value
});
}
----
#override
Widget build(BuildContext context) {
print("rebuild");
return Column(
children: [
ElevatedButton(
onPressed: () async {
final selectedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now().subtract(Duration(days: 4444)),
lastDate: DateTime.now().add(Duration(days: 4444)),
);
if (selectedDate == null) return;
final response = await getTimes(selectedDate);
print(response);
setState(() {
_isButtonDisabled = response != null;
});
},
child: Text("Select Date"),
),
ElevatedButton(
onPressed: _isButtonDisabled ? null : () {}, child: Text("t"))
],
);}
I'm trying to wrap a ValueListenableBuilder around a Textfield which has a functionality of taking input text and returning the same text. The original purpose is to persist the input data through a database. But while implementing the basic code given below, I'm getting the error "Expected a value of type 'TextEditingController', but got one of type 'TextEditingValue'". Could you please enlighten me on the error?
import 'package:flutter/material.dart';
void main() => runApp(MyTextFieldApp());
class MyTextFieldApp extends StatelessWidget {
final _controller = TextEditingController();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.white,
body: Container(
padding: EdgeInsets.all(24.0),
child: Center(
child: ValueListenableBuilder(
valueListenable: _controller,
builder: (BuildContext context, _controller, _ ) {
return TextField(
autofocus: true,
maxLines: 6,
controller: _controller,
decoration: InputDecoration(
labelText: "Note",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
);
},
),
)
)
)
);
}
}
You are not pass a TextEditingController to the TextField
ValueListenableBuilder(
valueListenable: _controller,
builder: (BuildContext context, _controller, _ ) {
// this _controller is not equal to the valueListenable: _controller above, it means _controller.value
return TextField(
autofocus: true,
maxLines: 6,
controller: _controller,
decoration: InputDecoration(
labelText: "Note",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
);
},
),
ValueListenableBuilder(
valueListenable: _controller,
builder: (BuildContext context, _value, _ ) {
return TextField(
autofocus: true,
maxLines: 6,
controller: _controller,// assign the TextEditingController
decoration: InputDecoration(
labelText: "Note",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
);
},
),
You should do the following:
import 'package:flutter/material.dart';
void main() => runApp(MyTextFieldApp());
class MyTextFieldApp extends StatelessWidget {
final _controller = TextEditingController();
final ValueNotifier valueNotifier = ValueNotifier("initial");
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar( title : Text("Title")),
backgroundColor: Colors.white,
body: Container(
padding: EdgeInsets.all(24.0),
child: Center(
child: ValueListenableBuilder(
valueListenable: valueNotifier,
builder: (BuildContext context, values, child ) {
return Column(
children : <Widget>[
TextField(
autofocus: true,
maxLines: 6,
controller: _controller,
decoration: InputDecoration(
labelText: "Note",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
),
RaisedButton(child : Text("click me"),onPressed : (){
valueNotifier.value = _controller.text;
}),
Text(values),
],
);
},
),
)
)
)
);
}
}
From the docs:
ValueListenableBuilder<T>
A widget whose content stays synced with a ValueListenable.
Given a ValueListenable and a builder which builds widgets from concrete values of T, this class will automatically register itself as a listener of the ValueListenable and call the builder with updated values when the value changes.
The valueListenable property is of type ValueListenable<T>, which is an interface implemented by ValueNotifier<T>.
Therefore you need to create an instance of ValueNotifier<T>:
final ValueNotifier valueNotifier = ValueNotifier("initial");
In this case, I created it with type String with an intial value initial. Then assign this instance to the property valueListenable:
valueListenable: valueNotifier,
The builder which is of type ValueWidgetBuilder<T> will only get called when valueNotifier is updated.
Therefore you can create a RaisedButton and onPressed, you can update the valueNotifier value, which will call the builder and update the Text widget.
https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html
If you wanna use TextEditingController with ValueNotifier to handle real time when user input anything in TextField/TextFormField like picture below:
Ex: For my prj, I'm handling to display or not the Text('This email not match format ...') below the Email TextField when the user input from keyboard.
If matched with the email format: hide the Text (red Color).
If not match: show the Text (red Color).
So, in order to handle realtime, you can wrap your TextFormField (with TextEditingController related) by ValueListenableBuilder with valueListenable is editingController corresponding.
CustomTextField(
hintText: R.strings.emailHint!,
labelTextField: "Email",
isRequired: true,
textController: editEmailController, // use editEmailController for TextField related.
focusNode: editEmailFocusNode,
textInputType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
ValueListenableBuilder(
valueListenable: editEmailController, // no need to define the variable of ValueNotifier, use directly the textEditingController variable of TextFormField related.
builder: (_, value, __) {
return editEmailController.text.trim().isNotEmpty &&
validateEmail(editEmailController.text.trim()) ==
false
? Align(
alignment: Alignment.centerLeft,
child: Text(
R.strings.emailNotMatchFormat!,
style: const TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12,
color: Colors.redAccent,
),
),
)
: const SizedBox();
},
),
I'm building an app with Flutter and Bloc like architecture. I'm trying to call submit func with not only login button but also keyboard done button with password; only when email and password are valid.
I could implement login button version with combineLatest, but I'm not sure keyboard version. I need to validate both email and password when keyboard done button pressed before calling submit. I could nest streamBuilder, but I feel it is not good practice.
Is there any way to get the latest value from combineLatest? BehaviorSubject<bool>().value Or any possible advice to implement this.
sample code:
final _emailController = BehaviorSubject<String>();
final _passwordController = BehaviorSubject<String>();
// Add data to stream
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password =>
_passwordController.stream.transform(validatePassword);
Stream<bool> get submitValid =>
Observable.combineLatest2(email, password, (e, p) => true);
// change data
Function(String) get changeEmail => _emailController.sink.add;
Function(String) get changePassword => _passwordController.sink.add;
submit() {
final validEmail = _emailController.value;
final validPassword = _passwordController.value;
print('Email is $validEmail, and password is $validPassword');
}
import 'package:flutter/material.dart';
import '../blocs/bloc.dart';
import '../blocs/provider.dart';
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of(context);
return Container(
margin: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
emailField(bloc),
passwordField(bloc),
Container(
margin: EdgeInsets.only(top: 25.0),
),
submitButton(bloc),
],
),
);
}
Widget emailField(Bloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'ypu#example.com',
labelText: 'Email Address',
errorText: snapshot.error,
),
);
},
);
}
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Password',
labelText: 'Password',
errorText: snapshot.error,
),
textInputAction: TextInputAction.done,
onSubmitted: () {}, // <- here
);
});
}
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: snapshot.hasData ? bloc.submit : null,
);
},
);
}
}
You can wrap Your Password streamBuilder with another streamBilder and onSubmitted call submit method.
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snap) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Password',
labelText: 'Password',
errorText: snapshot.error,
),
textInputAction: TextInputAction.done,
onSubmitted: snap.hasData ? bloc.submit : null,
);
});
});
}