In Flutter project, I need to listen to the input text in TextFormField and do certain actions, especially when user put some character (eg. space) in this filed or when he request focus. When this kind of event happen, I need to modify filed's value.
I know there is a property called controller but I don't know how to use it in this case.
Thank you in advance.
You can specify a controller and focus node, then add listeners to them to monitor for changes.
Ex:
Define controllers and focus nodes
TextEditingController _controller = new TextEditingController();
FocusNode _textFocus = new FocusNode();
Define listener function
void onChange(){
String text = _controller.text;
bool hasFocus = _textFocus.hasFocus;
//do your text transforming
_controller.text = newText;
_controller.selection = new TextSelection(
baseOffset: newText.length,
extentOffset: newText.length
);
}
Add listner to controller and focusnode at initState
// you can have different listner functions if you wish
_controller.addListener(onChange);
_textFocus.addListener(onChange);
Then you can use it as
new TextFormField(
controller: _controller,
focusNode: _textFocus,
)
Hope that helps!
If you are just trying to transform the input to other form in TextFormField, you better use "TextInputFormatter". Using a listener with TextController cause a lot of troubles. Take a look at my sample code see if that helps you. btw, The last line of code is just trying to move the cursor to the end of text.
TextFormField(inputFormatters: [QuantityInputFormatter()])
class QuantityInputFormatter extends TextInputFormatter {
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final intStr = (int.tryParse(newValue.text) ?? 0).toString();
return TextEditingValue(
text: intStr, selection: TextSelection.collapsed(offset: intStr.length),);
}
}
Related
I have a list of some entries I want edit on focus out. I createe FocusNode for each entry, CupertinoTextField for each entry too.
var textField = (UserMotivator um) {
var controller;
var focusNode = new FocusNode();
focusNode.addListener(() {
if (!focusNode.hasFocus) {
post(um);
}
});
var controller = TextEditingController(text: um.text);
return CupertinoTextField(
focusNode: focusNode,
controller: controller,
onChanged: (String value) {
um.text = value;
}
);
};
For some weird reason, in simulator (not tested on real device), when I click on many of these TextFields, I get this:
How do I bound a focus out even to a TextField without using FocusNode/ without having all of these cursors blinking?
So I resolved the issue I think. The reason it was buggy was that I was on v1.1.8, after updating to v1.5.4 it somehow got fixed, was not perfect but was better. After I moved the FocusNodes creation code form build to initState method it got even better but the cursor was still blinking at the start of the TextField. This was because I had called setState in the onChange handler, which somehow caused the TextField to redraw and act so weirdly.
So I'm working on a TextField with scoped_model. I moved the controller to the Model class, and I'm trying to change the state of a Text with the text the user inputs on said TextField. But when I close the keyboard, the state changes and the TextField is now empty, so nothing is seen on the Text widget either. This is my code:
CupertinoTextField(
controller: model.lastNameController,
onChanged: (text) => model.changeShortLastNameState(lastName: text),
),
And this is the relevant code on my Model
final lastNameController = TextEditingController();
void changeShortLastNameState({String lastName}) {
var splitLastName = lastName.split(' ');
var shortLastName = splitLastName[0];
this.shortLastName = shortLastName;
notifyListeners();
}
I found someone on the flutter github with the same issue, but they sent him to SO, and I haven't been successful on finding a question by the same guy. Does anyone know how to solve this issue? Thanks.
Use the deceleration of text editing controller out side of build function.
I am using a normal bottom navigation bar with a local state containing the index of the currently selected page. When tapping on one of the nav items the tapped page is displayed:
class _HomePageState extends State<HomePage> {
int _selectedIndex = 1;
final _widgetOptions = [
Text('Index 0'),
Overview(),
Details(),
Text('Index 3'),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
...
currentIndex: _selectedIndex,
onTap: onTap: (index) => _selectedIndex = index,
),
);
}
}
How to proceed if you want to navigate from inside one page to another and update the selected index of the bottom navigation bar?
Example: Page 2 is an overview list which contains among other things the ID of the list items. If you tap an item in the list, it should be navigated to the details page (3 in bottom nav bar) displaying details for the selected item and therefore the ID of the selected item should be passed.
Navigator.of(context)... can't be used, because the individual pages are items of the nav bar and therefore not routes.
You will need a better state management way. I advise you to use BLoC pattern to manage navigation changes in this widget. I will put a simplified example here about how to do this with some comments and external reference to improvements.
// An enum to identify navigation index
enum Navigation { TEXT, OVERVIEW, DETAILS, OTHER_PAGE}
class NavigationBloc {
//BehaviorSubject is from rxdart package
final BehaviorSubject<Navigation> _navigationController
= BehaviorSubject.seeded(Navigation.TEXT);
// seeded with inital page value. I'am assuming PAGE_ONE value as initial page.
//exposing stream that notify us when navigation index has changed
Observable<Navigation> get currentNavigationIndex => _navigationController.stream;
// method to change your navigation index
// when we call this method it sends data to stream and his listener
// will be notified about it.
void changeNavigationIndex(final Navigation option) => _navigationController.sink.add(option);
void dispose() => _navigationController?.close();
}
This bloc class expose a stream output currentNavigationIndex. HomeScreen will be the listener of this output which provides info about what widget must be created and displayed on Scaffold widget body. Note that the stream starts with an initial value that is Navigation.TEXT.
Some changes are required in your HomePage. Now we're using StreamBuilder widget to create and provide a widget to body property. In some words StreamBuilder is listening a stream output from bloc and when some data is received which will be a Navigation enum value we decide what widget should be showed on body.
class _HomePageState extends State<HomePage> {
final NavigationBloc bloc = new NavigationBloc();
int _selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<Navigation>(
stream: bloc.currentNavigationIndex,
builder: (context, snapshot){
_selectedIndex = snapshot.data.index;
switch(snapshot.data){
case Navigation.TEXT:
return Text('Index 0');
case Navigation.OVERVIEW:
// Here a thing... as you wanna change the page in another widget
// you pass the bloc to this new widget for it's capable to change
// navigation values as you desire.
return Overview(bloc: bloc);
//... other options bellow
case Navigation.DETAILS:
return Details(/* ... */);
}
},
),
bottomNavigationBar: BottomNavigationBar(
//...
currentIndex: _selectedIndex,
onTap: (index) => bloc.changeNavigationIndex(Navigation.values[index]),
),
);
}
#override
void dispose(){
bloc.dispose();// don't forgot this.
super.dispoe();
}
}
Since you wanna change the body widget of HomePage when you click in a specific items that are in other widget, Overview by example, then you need pass the bloc to this new widget and when you click in item you put new data into Stream that the body will be refreshed. Note that this way of send a BLoC instance to another widget is not a better way. I advise you take a look at InheritedWidget pattern. I do here in a simple way in order to not write a bigger answer which already is...
class Overview extends StatelessWidget {
final NavigationBloc bloc;
Overview({this.bloc});
#override
Widget build(BuildContext context) {
return YourWidgetsTree(
//...
// assuming that when you tap on an specific item yout go to Details page.
InSomeWidget(
onTap: () => bloc.changeNavigationIndex(Navigation.DETAILS);
),
);
}
}
I know so much code it's a complex way to do this but it's the way. setState calls will not solve all your needs. Take a look at this article It's one of the best and the bigger ones that talks about everything that I wrote here with minimum details.
Good Luck!
You can use Provider to manage the index of the BottomNavigationBar.
Make a class to handle the indexPage and consume it in your widgets using the Consumer. Don't forget to register the ChangeNotifierProvider in the
ChangeNotifierProvider<NavigationModel> (create: (context) => NavigationModel(),),
class NavigationModel extends ChangeNotifier {
int _pageIndex = 0;
int get page => _pageIndex;
changePage(int i) {
_pageIndex = i;
notifyListeners();
}
}
make some change like..
int _currentIndex = 0;
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
body: _widgetOptions[_currentIndex],
tab click..
onTap: onTabTapped,
I have a page in Flutter with a couple of widgets including a TextField (lets call this View1). When the user clicks the TextField I rebuild the page showing only the TextField and keyboard (lets call this View2). When the user finishes with the TextField I rebuild the page again showing all the widgets as before (lets call this View3 although note that it is the same as View1). This works fine except for one thing. I get a temporary yellow/black indicator (in debug mode) showing that there is not enough room to display all of the widgets in View3. The indicator only lasts a short time and I eventually worked out that it appears becasue Flutter is trying to display all the widgets while the keyboard has not yet finished animating away. Once the keyboard has finished animating away the yellow/black bar disappears and this explains I think why it only appears briefly.
It would be neat to request the build of View3 in a callback that executes when the keyboard has finished animating away but I don't see any way of doing that. Perhaps I'm missing something?
Another way I can think of to work around this would be to insert a delay before building View3 to allow time for the keyboard to disappear but this seems a little hacky. Has anyone any other ideas?
EDIT
I added a delay as follows and it works. Still seems a bit hacky though.
Timer(Duration(milliseconds: 500),(){setState((){});});
Try using WidgetsBindingObserver and override the didChangeMetrics method like this:
class KeyboardTogglePage extends StatefulWidget {
#override
_KeyboardTogglePageState createState() => _KeyboardTogglePageState();
}
class _KeyboardTogglePageState extends State<KeyboardTogglePage>
with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
var isKeyboardOpen = false;
///
/// This routine is invoked when the window metrics have changed.
///
#override
void didChangeMetrics() {
final value = MediaQuery.of(context).viewInsets.bottom;
if (value > 0) {
if (isKeyboardOpen) {
_onKeyboardChanged(false);
}
isKeyboardOpen = false;
} else {
isKeyboardOpen = true;
_onKeyboardChanged(true);
}
}
_onKeyboardChanged(bool isVisible) {
if (isVisible) {
print("KEYBOARD VISIBLE");
} else {
print("KEYBOARD HIDDEN");
}
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: TextField(),
),
);
}
}
When I put all mentioned solutions together, it works well for me:
class KeyboardTogglePage extends StatefulWidget {
#override
_KeyboardTogglePageState createState() => _KeyboardTogglePageState();
}
class _KeyboardTogglePageState extends State<KeyboardTogglePage>
with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// This routine is invoked when the window metrics have changed.
#override
void didChangeMetrics() {
final value = WidgetsBinding.instance.window.viewInsets.bottom;
bool isVisible = value > 0;
print("KEYBOARD VISIBLE: ${isVisible}");
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: TextField(),
),
);
}
}
this usually happens when the view1 that is the previous screen that the navigator is popping to has a column in it. try changing your column to listview and this should fix the issue for you.
Is it possible to preload somehow the image on the app start? Like I have an background image in my drawer but for the first time when I open the drawer I can see the image blinks like it is fetched from assets and then displayed and it gives bad experience to me once I see it for the first time other openings of the drawer are behaving as expected because it is cached. I would like to prefetch it on the app load so there is no such effect.
Use the precacheImage function to start loading an image before your drawer is built. For example, in the widget that contains your drawer:
class MyWidgetState extends State<MyWidget> {
#override
void initState() {
// Adjust the provider based on the image type
precacheImage(new AssetImage('...'));
super.initState();
}
}
I had problems with the upper solution which uses precacheImage() inside initState. The code below resolved them. Also note that you might not see expected results in the debug mode but only in the release mode.
Image myImage;
#override
void initState() {
super.initState();
myImage= Image.asset(path);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(myImage.image, context);
}
There is a good article on this subject : https://alex.domenici.net/archive/preload-images-in-a-stateful-widget-on-flutter
It should look something like this
class _SampleWidgetState extends State<SampleWidget> {
Image image1;
Image image2;
Image image3;
Image image4;
#override
void initState() {
super.initState();
image1 = Image.asset("assets/image1.png");
image2 = Image.asset("assets/image2.png");
image3 = Image.asset("assets/image3.png");
image4 = Image.asset("assets/image4.png");
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(image1.image, context);
precacheImage(image2.image, context);
precacheImage(image3.image, context);
precacheImage(image4.image, context);
}
#override
Widget build(BuildContext context) {
return Container(
child: Stack(
children: <Widget>[
image1,
image2,
image3,
image4,
],
),
);
}
}
To get rid of the "blink", you can simply use the FadeInImage class in combination with transparent_image, which will fade instead of appearing instantly. Usage, in your case, looks as follow:
// you need to add transparent_image to your pubspec and import it
// as it is required to have the actual image fade in from nothing
import 'package:transparent_image/transparent_image.dart';
import 'package:flutter/material.dart';
...
FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: AssetImage('image.png'),
)
The precacheImage() function (as used in most of the answers) returns a Future, and for certain use case scenarios it can be really useful. For instance, in my app I needed to load an external image and like everyone else did not want to experience the flickering. So I ended up with something like this:
// show some sort of loading indicator
...
precacheImage(
NetworkImage(),
context,
).then((_) {
// replace the loading indicator and show the image
// (may be with some soothing fade in effect etc.)
...
});
Please note that, in the above example I wanted to illustrate how the Future can be potentially used. The comments are just to help express the idea. Actual implementation has to be done in a Fluttery way.
I had the following problem: image requires some space. So after loading, it pushed UI down which is not good for UX. I decided to create a builder to display an empty (or loading) container until the image is loaded and after that display my UI.
Looks like precacheImage returns future which is resolved. The tricky part for me was FutureBuilder and snapshot.hasData which was always false because future is resolved with null. So I added future transformation to fix snapshot.hasData:
precacheImage(this.widget.imageProvider, context).then((value) => true)
I'm not sure is it ok to call precacheImage multiple times so I decided to wrap it into StatefulWidget.
You can check the final builder here
Use builder the following way:
return PreloadingImageBuilder(
imageProvider: AssetImage("assets/images/logo-main.png"),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
...
);
} else {
return Container();
}
},
);
Depending on your logic and UX, something like this can work too (when pushing a new route with images):
Future.wait([
precacheImage(AssetImage("assets/imageA.png"), context),
]).then((value) {
Navigator.pop(context);
Navigator.pushReplacement(
context,
YourRouteRoute()
);
});
late AssetImage assetImage;
#override
void initState() {
assetImage = const AssetImage("$kImagePath/bkg2.png");
super.initState();
}
#override
void didChangeDependencies() {
precacheImage(assetImage, context);
super.didChangeDependencies();
}