I'm making a flutter application where user can type a message and hit send button IN THE KEYBOARD to send the message. The problem is when I press the send button the message gets send but the keyboard gets automatically dismissed. How can I prevent that from happening?
Thanks in advance.
TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: new InputDecoration.collapsed(
hintText: "Let's talk",
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(1),
),
),
textInputAction: TextInputAction.send,
onSubmitted: null,
)
TextField widget has a parameter just for this purpose!
While onSubmit callback can be used to handle business logic when the user presses done, there is also a property called onEditingComplete, specially made to handle focus-related logic. So you should use both, for better code readability.
According to the docs:
The default implementation of onEditingComplete executes 2 different
behaviors based on the situation:
When a completion action is pressed, such as "done", "go", "send", or
"search", the user's content is submitted to the controller and then
focus is given up.
When a non-completion action is pressed, such as "next" or "previous",
the user's content is submitted to the controller, but focus is not
given up because developers may want to immediately move focus to
another input widget within onSubmitted.
Therefore, if you don't like this behaviour, for example, "send" is considered a "completion action" here, thus in an Instant Messaging (chatting) app, each time user sends a short message, the keyboard will be collapsed. But if we override the onEditingComplete callback to an empty function, it will stop the default behavior and not hide the keyboard.
Sample code:
TextField(
controller: _controller,
onSubmitted: (value) {
sendMessage(text);
_controller.clear();
},
onEditingComplete: () {}, // this prevents keyboard from closing
textInputAction: TextInputAction.send,
)
Demo:
For complete explanation and comparison between onSubmitted and onEditingComplete callbacks, check out my other answer here.
This worked for me:-
First Create a FocusNode and assign it to your textfield, do the following :-
The FocusNode is a long lived component so initialize it in the initState method:-
FocusNode inputFieldNode;
#override
void initState() {
super.initState();
inputFieldNode = FocusNode();
}
Do not forget to cleanup the FocusNode in dispose method after the Form is disposed:-
#override
void dispose() {
inputFieldNode.dispose();
super.dispose();
}
Assign the FocusNode to the textfield and write the following code in onSubmitted::-
TextField(
focusNode: inputFieldNode,
onSubmitted: (String) => FocusScope.of(context).requestFocus(inputFieldNode),
)
Now the textfield will not lose focus even after pressing the submit button.
The cleanest approach would be to use onEditingComplete() with TextEditingController instead of onSubmitted(text). Refer below example.
final _controller = TextEditingController();
TextField(
controller: _controller,
padding: EdgeInsets.all(8),
textInputAction: TextInputAction.send,
placeholder: 'Type your message',
onEditingComplete: (){
if (_controller.text.isEmpty) return;
sendMessage(_controller.text);
},
),
// Focus node
FocusNode _myTextFieldFocusNode = FocusNode();
Widget build(BuildContext context) {
return SafeArea(
body: Focus(
onFocusChange: (hasFocus) => !hasFocus ? _myTextFieldFocusNode.requestFocus() : null
Container(
child: // child goes here
)
),
);
}
If the user taps to something like it can "dismiss the keyboard" Focus() widget will detect
changes and this is where you might want to put your _myTextFieldFocusNode.requestFocus()
keyboard won't have a chance to dismiss and reopen again
SystemChannels.textInput.invokeMethod('TextInput.show');
on onSubmit method
Related
As soon as a TextField gets focused, an object which stores if it's enabled changes so that the TextField gets disabled immediately. This also happens when another TextField above gets focused.
The TextField is placed inside a StatefulWidget and a Category object contains another object called Goal which contains a bool variable if it's enabled or disabled. This variable is also used to enable or disable the TextField.
TextField(
controller: _goalAmountController,
enabled: widget.category.goal.enabled,
decoration: InputDecoration(
labelText: "Goal Amount",
border: OutlineInputBorder(),
),
onChanged: (value) {
try {
widget.category.goal.amount = double.parse(value);
} on Exception {
//TODO display error message
print("Invalid Goal-Amount");
}
},
),
There's also a switch below the TextField to enable or disable the Goal by setting it's enabled variable.
SwitchListTile(
value: widget.category.goal.enabled,
title: Text("Enable Goal"),
onChanged: (value) {
setState(
() {
widget.category.goal.enabled = value;
},
);
},
),
I found out that it seems as if the click on a TextField would replace the Goal object with a new one which has false as the default value for enabled.
Try to use FocusNodes instead :
FocusNode textNode = FocusNode();
TextField(
focusNode: textNode,
controller: _goalAmountController,
enabled: widget.category.goal.enabled,
decoration: InputDecoration(
labelText: "Goal Amount",
border: OutlineInputBorder(),
),
later when you want to disable this textField when the user interact with another widget you can call:
textNode.unfocus() ;
I am using flutter to make a simple application.
In the below code, when TextBox changed event is fired then I call the method named updateTitle().
But I have to call the same method updateTitle(), when key is up, as we use in javascript and other languages too.
TextField(
controller: titleController,
style: textStyle,
onChanged: (value) => updateTitle(),
decoration: InputDecoration(
labelText: "Title",
labelStyle: textStyle,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
)),
),
For your use case, the onChangedworks just like onkyeup would work. Everytime that the user tap a new character in the textfield, it is fired.
In order to enable/disable a button you should listen to this event, do the test to see if the field isn't empty, modify a variable that will handle the button state, and call setState().
This is just a sample code. Not tested, but should work as is.
class _SoAnswerState extends State<SoAnswer> {
bool _buttonActive = false;
#override
Widget build(BuildContext context) {
...
TextField(
controller: titleController,
style: textStyle,
onChanged: (value) => updateButtonState(value), // onChanged return the value of the field
decoration: InputDecoration(
labelText: "Title",
labelStyle: textStyle,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
)
),
),
...
}
void updateButtonState(String text){
// if text field has a value and button is inactive
if(text != null && text.length > 0 && !_buttonActive){
setState(() {
_buttonActive = true;
}
}else if((text == null || text.length == 0) && _buttonActive){
setState(() {
_buttonActive = false;
}
}
}
Edit: add more information about the events
In JavaScript, the onkeyup event handler fires when the user releases a key that was previously pressed. When the user press and release a key inside a text field, the text field value changes. The onChanged listener in Flutter fires when the text field value changes. When working with a typing interface, where the user uses a tradicional keyboard, it is not a good idea to listen to all the changes of a text field, because the user can press and hold a key, leading the application to repeat the onchanged event too many times, once for every character repetition. That's not the case with a mobile interface, where the user (usually) can't press and hold a key.
The following code builds and runs as intended--when a user types something, an error message gets shown until the string passes an email validation format.
Widget emailField(){
return StreamBuilder(
stream: bloc.emailStream,
builder: (BuildContext context, snapshot) {
return TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'E-mail address',
errorText: snapshot.error
),
onChanged: (newValue){
bloc.updateEmail(newValue);
},
);
},
);
}
I am told that when the stream changes, the builder field gets called which rebuilds the TextField. But if that is the case, shouldn't the TextField always have a blank string? What happens instead is it retains its value.
I am trying to understand what exactly is happening here. Thanks!
Not really, If you look at the code of the TextField , you'll find it's a StatefulWidget, so its has a State and the state keep the value.
class TextField extends StatefulWidget
Additionally you can use the TextEditingController to handle(get/clear/set) the data of the TextField , if you don't provide a TextEditingController it will be created by default as you can see in the source code.
#override
void initState() {
super.initState();
if (widget.controller == null)
_controller = TextEditingController();
}
You can find more information here: https://flutter.io/cookbook/forms/text-field-changes/
TextField widget is newly created, but it's corresponding element is reused/recyled (and so is it state).
Here's a pretty good explanation from the creator: https://youtu.be/AqCMFXEmf3w?t=99
I am getting started with the BLoC pattern but I have a question:
1) Should you use the BloC pattern to determine if the routes should change?
Example: authentication object changes to unauthenticated so the listeners should handle the route changes.
2) Should the BLoC pattern only be used for UI state and handle the route changes on UI changes?
Example: User clicks on login and navigates to the home screen.
I ask this question because I'm facing a problem where I don't have a central navigation management solution.
This code is in my BLoC now:
loggedIn.listen((AuthResponse user) {
currentUserSubject.add(user);
Navigator.pushReplacement(
_context,
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) {
return HomePage();
},
transitionsBuilder: (context, animation, _, child) {
return new SlideTransition(
child: child,
position: new Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(animation),
);
},
transitionDuration: Duration(milliseconds: 400),
),
);
}, onError: (error) {
Scaffold.of(_context).showSnackBar(new SnackBar(
content: new Text(error.message),
));
});
What I'm currently doing is:
Register a NavigatorBloc that receive a NavigatorState in the constructor and receive actions to navigate to different pages in your app Ex: GoToHomePageAction, GoToRegisterPageAction, NavigatorActionPop.
In your App widget you register the NavigatorBloc and provide the navigator key attached to the MaterialApp widget.
#override
Widget build(BuildContext context) {
return BlocProvider<NavigatorBloc>(
bloc: NavigatorBloc(navigatorKey: widget.navigatorKey),
child: MaterialApp(
navigatorKey: widget.navigatorKey,
title: 'Medicine Tracker',
theme: ThemeData(
primarySwatch: Colors.red,
scaffoldBackgroundColor: Colors.white
),
home: HomePage(),
),
);
}
Then in side your NavigatorBloc you just check the the action and navigate to the desired page:
class NavigatorBloc extends Bloc<NavigatorAction, dynamic>{
final GlobalKey<NavigatorState> navigatorKey;
NavigatorBloc({this.navigatorKey});
#override
dynamic get initialState => 0;
#override
Stream<dynamic> mapEventToState(NavigatorAction event) async* {
if(event is NavigatorActionPop){
yield navigatorKey.currentState.pop();
}
}
}
Hope it help.
I see there are 2 Yes/No questions there (in contrast to W/H questions), and my answers are yes to both of them. The reason being that with BloC and navigation, you can actually recover the current screen if the app crashes (auto-save state and state recover must be in place, but it's another W/H question), and other nice features of BLoC/event-based state management (history snapshot, time machine, event replay, separate of concerns, testability, etc.)
I ask this question because I'm facing a problem where I don't have a central navigation management solution.
Is there anything I can help with regarding your problem?
I am thinking of this pattern for my application, and cannot find a proper way of doing it.
User is in list page, she clicks on an item and goes to edit page. She edits, and hits the save. Now I want the application to
- show spinning wheel, while saving the item
- after save pop the route to go back to list page, AND show a snackbar on the list page.
I don't seem to find a way to pass a parameter to the previous route to tell it about the state, which is to show the snackbar.
For showing progress on click of save button you can use something like this.
Firstly initialize a variable static bool isSaving = false; then in your scaffold use it like this.
child:Stack(
children: <Widget>[
isSaving
? new Container(
child: new CircularProgressIndicator(
value: null,
strokeWidth: 2.0,
),
alignment: Alignment.center,
)
: new Container(
child: new RaisedButton(
onPressed: () {
setState(() {
isSaving = true;
//Other Implementations here
});
},
child: new Text("Save"),
),
),
],
),
When the operation is complete again set state and make variable false.
Now when you get notified that operation has been done use Navigator to go back.
Navigator.pop(context);
For showing snackbar in your previous screen use a Global Key.
GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
Assign this key to your scaffold.
new Scaffold(
key: scaffoldKey,
...
)
As answered here show snackbar just after pop operation like this.
scaffoldKey.currentState
.showSnackBar(new SnackBar(content: new Text("Saved Successfully")));
Or can try a different approach as explained in this sample.
Hope it helps.
You can send data back through pop method. You can do something like that.
In your edit page:
bool saved = item.save();
Navigator.pop(context, saved);
In your list page:
var saved = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => EditRoute(item)),
);
if (saved) {
// Show success snackbar
} else {
// Show error snackbar
}