Flutter - Dynamic suffix text for TextField? - flutter

I'm building a simple soundboard app in Flutter, which includes a search bar at the top of the application.
At the point where the search is activated, the interesting bit of the widget tree is as follows:
home: Scaffold(
appBar: AppBar(
title: new TextField(
controller: _filter,
decoration: new InputDecoration(
prefixIcon: new Icon(Icons.search, color: Colors.white),
hintText: 'Search...'
),
As text is typed into the search bar, a listener on the _filter updates a list of quotes which is used to build the body of the app. This is all working fine.
However, I would now like the app to show a count of the returned results, and since this is sitting in my header bar I'd like it to sit in-line with the bar itself, something like:
Things I've tried, all within the InputDecoration:
Setting suffixText - this is styled right and in the right place, however it doesn't update as the _filter changes because I'm not reconstructing the TextField every time (and I can't do this as it messes up what's been typed in).
Setting suffix to a full TextField widget with its own controller. This gets it auto-updating, but for some reason obscures my actual search text. I tried making the background transparent but that hasn't helped - new TextField(controller: _searchCountController, readOnly: true, textAlign: TextAlign.end, style: TextStyle(backgroundColor: Colors.transparent),). In the below screenshot, I've typed a 'w' which has updated the count to 84 (but I can't see the 'w' I've typed...)
Setting counter instead of suffix. I can get this to auto-update and not obscure my search, but it appears under the search bar which makes the whole thing look naff. Doesn't really seem appropriate for something sat in the title bar.
Any suggestions for how I can achieve what I'm after? Very new to Flutter so very possible that I've missed something obvious. Hoping there's a simple solution to this :)

Well, as expected it turned out I was missing something obvious and there was a simple solution. I was having trouble getting a simple suffixText to update because I was ultimately caching a component and therefore not giving Flutter a chance to re-render it.
In case it helps anyone, I had followed this post to implement a search bar. The pattern they use is to store an _appBarTitle widget which only gets changed when search is initiated and cancelled. I moved away from this approach, instead having a simple _searching boolean and allowing flutter to do the rest:
Widget _buildAppBar() {
if (_searching) {
return new TextField(
controller: _filter,
decoration: new InputDecoration(
prefixIcon: new Icon(Icons.search, color: Colors.white),
hintText: 'Search...',
suffixText: '${_filteredQuotes.length}',
),
autofocus: true,
style: TextStyle(color: Colors.white));
} else {
return new Text('Pocket Scat');
}
}
With this approach, I don't need to specify a component for the suffix at all. It also has the advantage of automatically scooping up hintColor from my app's style.

You can copy paste run full code below
You can use Row with Expanded(TextField) and Text()
code snippet
AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: TextField(
controller: _filter,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.white),
hintText: 'Search...',
)),
),
Text('$_counter'),
],
))
working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
TextEditingController _filter = TextEditingController();
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: TextField(
controller: _filter,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.white),
hintText: 'Search...',
)),
),
Text('$_counter'),
],
)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Related

Flutter GNav disappears when using Navigator.pushNamed()

I implemented an app that uses GNav from google_nav_bar.dart. Most things work fine and I really like it, i can manually switch pages by clicking on the tab icons of the navbar. But when I am trying to change the page in the code (e.g. Buttonpress or after the User did a certain action) with Navigator.pushNamed the navbar disappears. I know that this is normal bcs why should the navbar stay, but I have no idea how to manage this. There are not that many examples on the internet. Would be pretty nice if someone could help me!
Explanation of the app: the User scans a barcode of a book, and after the scan he gets redirected to the app page, where the user can find reviews to that book. And i want the navbar on that reviews page as well.
Here the simplified code where the navigation happens, the different screens just return a Scaffold, they are not implemented yet:
class MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
}
int _selectedIndex = 0;
static final List<Widget> _pages = <Widget>[
const ScannerScreen(),
const AllReviewsScreen(),
];
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('ReviewApp'),
),
body: _pages.elementAt(_selectedIndex),
bottomNavigationBar: Container(
color: Colors.black,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0),
child: GNav(
gap: 8,
backgroundColor: Colors.black,
color: Colors.white,
activeColor: Colors.white,
tabBackgroundColor: Colors.grey.shade800,
padding: const EdgeInsets.all(12),
tabs: const [
GButton(
icon: Icons.qr_code_scanner,
text: 'Scanner',
),
GButton(
icon: Icons.reviews,
text: 'Alle Reviews',
),
],
selectedIndex: _selectedIndex,
onTabChange: (index) {
setState(() {
_selectedIndex = index;
});
},
),
),
),
),
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/scanner':
return FadeRoute(const ScannerScreen());
case '/all_reviews':
return FadeRoute(const AllReviewsScreen());
default:
return null;
}
});
}
}
I tried to use a tabController, but GoogleNavBar has no controller, so I think it handles this another way, but I hardly found stuff on the internet, in every example they only switch screen using the navbar. Of course i could just implement the navbar on every screen, but there must be an easier way
It is obvious behavior because when you call Navigator. push it immediately pushes the UI to a new screen where your bottomAppBar is not available.
Here is an article about what you want:
Multiple Navigators with BottomNavigationBar

Underlines of TextFormFields in form not uniform no matter what I do. How to keep a uniform thickness for all underlines of TextFormFields?

I'm brand new to flutter and building a form in Flutter containing multiple TextFormFields, and a radio button. But, something weird is happening where the textformfields above the radio buttons are not uniform and keep changing randomly based on other changes I make. In my screenshot 1, you can see the first 3 text fields are uniform, but the one after the radio button is lighter. This is the closest I've come to achieving uniform text field underlines. I've tried adding and removing a lot of things such as padding, sized boxes, containers, etc, and they affect the underlines in different ways.
For instance, changing the mainAxisAlignment under Form->Column->Padding to 'start' instead of spaceEvenly causes the 1st 3 underlines to have thickness in increasing order and the last text box is fine Check screenshot 2. How can I make it so that all the underlines of the TextFormFields are equally thick and uniform irrespective of any other changes I make? Is there any better way of doing radio buttons than what I have done? Is there a better element in flutter than radio buttons for what I have done? I would also love it if I could make the radio buttons horizontal instead of vertical. The underlines are working fine and not fading in chrome, but they are in android emulator (Pixel 5 API30). I want this to work regardless of platform.
main.dart:
import 'package:flutter/material.dart';
import 'package:hospital_finance_app/pages/OPD.dart';
import 'package:hospital_finance_app/pages/login_page.dart';
import 'package:hospital_finance_app/utils/routes.dart';
import 'package:google_fonts/google_fonts.dart';
void main() {
runApp(const hospApp());
}
class hospApp extends StatelessWidget {
const hospApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Login Demo',
home: OPD(),
);
}
}
OPD.dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hospital_finance_app/utils/routes.dart';
enum paymentMethod { Cash, Other }
class OPD extends StatefulWidget {
#override
State<OPD> createState() => _OPDState();
}
class _OPDState extends State<OPD> {
paymentMethod? _method = paymentMethod.Cash;
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
appBar: AppBar(backgroundColor: Colors.black),
body: Form(
child:Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextField(
decoration: InputDecoration(
labelText: "Name",
hintText: "Name",
),
),
TextFormField(
decoration: InputDecoration(
hintText: "Mobile number",
labelText: "Mobile Number"),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
),
TextFormField(
decoration: InputDecoration(
labelText: "Amount",
hintText: "Amount"),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
),
Container(
child: Text("Mode of Payment", style: TextStyle(fontSize: 16, color: Colors.black54, fontFamily: "OpenSans", fontWeight: FontWeight.w400 ), textAlign: TextAlign.left),alignment: Alignment.centerLeft,),
Column(
children: <Widget>[
ListTile(
title: const Text('Cash'),
leading: Radio<paymentMethod>(
value: paymentMethod.Cash,
groupValue: _method,
onChanged: (paymentMethod? value) {
setState(() {
_method = value;
});
},
),
),
ListTile(
title: const Text('Other'),
leading: Radio<paymentMethod>(
value: paymentMethod.Other,
groupValue: _method,
onChanged: (paymentMethod? value) {
setState(() {
_method = value;
});
},
),
),
],
),
TextFormField(
decoration: InputDecoration(labelText: "Comments/Notes",hintText: "Comments/Notes"),
),
ElevatedButton(
onPressed:(){ Navigator.pushNamed(context, MyRoutes.opdRoute); },
child: Text("Submit"),
style: TextButton.styleFrom(minimumSize: Size(100,30)),)
]
),
)
),
));
}
}
Screen shot of my application on android emulator with mainAxisAlignment spaceEvenly
Screen shot of my application on android emulator with mainAxisAlignment start.
this is a bug from the android emulator. try running your app on a real device.
When you use mainAxisAlignment as 'start', all the TextFormFields are placed one after another without any spacing between them, so the line thickness is increased (two lines overlap).
Instead, when you spaceEvenly, TextFormFields are separated and the line appears thin, the original thickness.

How do I deal with this insanity of Flutter's closing requirements?

I'm constantly getting stuck with the ]'s, }'s and )'s at the end of my widgets:
Expanded(
child: TextFormField(
controller: feedbackController,
cursorColor: Colors.white,
style: TextStyle(
color: Colors.white,
),
decoration: InputDecoration(labelText: 'text input'),
)
]),
)
)
]),
I don't know how to deal with this. I find myself wasting so much time just guessing at how to close my triple nested widgets nested within double widgets nested within whatever. It's too convoluted. Is there a finishing tool for building UI's in flutter? An addon that'll close these out for me?
edit: here's what finally closed it.
)
)]))]
)
)]);
}
}
You can format the document to see the widget tree correctly, but basically in your code that you showed above it should be like this:
Expanded(
child: TextFormField(
cursorColor: Colors.white,
style: TextStyle(
color: Colors.white,
),
decoration: InputDecoration(labelText: 'text input'),
)),
Expanded is a class. In the above code you are initializing the class Expanded, and the constructor takes a required child argument of type Widget. Thus you use the class TextFormField.
So basically whenever you use a class you need to use (), each class will have many properties which you seperate them using , (example style is a property which has a value of type TextStyle so you initialize the class and close it ),
You use [] when you have a list example the column widget has a children property which is of type List<Widget>. Finally, you use ; before using }. The semicolon indicates an end of statement, example:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Text("test")
);
}

How to update a custom Stateful Widget using Floating Action Button

I've recently started using Flutter just for fun, and I'm stuck on adding actual functionality to the code without having everything inside one class.
Essentially, I'm trying to use a FloatingActionButton to increment the value of a Text Widget which stores the value of the user's level as an integer, but I don't want to have the whole app as a StatefulWidget because only the level is going to be updated. When the button is pressed, the value should increment by 1 and then show the new value on the screen.
I have the Level Text Widget inside a StatefulWidget class along with a function to update the level by one and set the state; the MaterialApp inside a StatelessWidget class; and the main body code inside another StatelessWidget class.
If this isn't the best way to do it please do let me know so I can improve for future projects, thanks.
main.dart
import 'package:flutter/material.dart';
main() => runApp(Start());
/// The Material App
class Start extends StatelessWidget{
#override
Widget build(BuildContext context){
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.grey[800],
appBar: AppBar(
title: Text("Home Page"),
backgroundColor: Colors.cyan,
centerTitle: true,
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.orange,
child: Icon(Icons.add, color: Colors.black,),
),
body: HomePage(),
),
);
}
}
/// Main Content for the page (body)
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// removed other children so there's less code to scan through for you :)
Padding(
padding: EdgeInsets.fromLTRB(30, 0, 0, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Text that just says "Level"
Text(
"Level",
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 32,
),
),
// space between text and actual level value
SizedBox(height: 10),
// Create new level widget
Level(),
],
),
),
],
),
);
}
}
/// Updating level using a Stateful Widget
class Level extends StatefulWidget{
#override
State<StatefulWidget> createState(){
return _LevelState();
}
}
class _LevelState extends State<Level>{
int level = 0;
void incrementLevel(){
setState(() {
level += 1;
});
}
#override
Widget build(BuildContext context){
return Text(
"$level",
style: TextStyle(
color: Colors.grey[900],
fontWeight: FontWeight.normal,
fontSize: 28,
),
);
}
}
It actually is a weird way of doing it. However, there is various ways of achieving this
To give an example:
You can use KEYs to remotely redraw the child state
If you want an advanced solution that can assist you in bigger projects. You can use state management tecniques. You can find a lot of tutorials in the internet but these are some of them. BLOC, Provider, InheritedWidget.
Basicaly all of them does the same thing. Lifts up the state data so the place of the redrawn widget on the widget tree will not be important.
I strongly encourage you to watch some tutorials starting with the Provider. I hope this helps

Flutter Dynamic maxHeight

I build a scrolling textfield like proposed in https://github.com/flutter/flutter/issues/9365. Now I want the maxHeight of the ConstrainedBox change dynamically, according to the shown or not shown keyboard. Is there a way to accomplish this?
Widget _buildTextInput() {
return new Container(
padding: new EdgeInsets.all(7.0),
child: new ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 150.0 <-- This should be dynamic
),
child: new SingleChildScrollView(
scrollDirection: Axis.vertical,
reverse: true,
// here's the actual text box
child: new TextField(
keyboardType: TextInputType.multiline,
maxLines: null, //grow automatically
decoration: new InputDecoration.collapsed(
hintText: 'Please enter a lot of text',
),
),
),
),
);
}
The red box should be the constrained box with open keyboard.
And like so with a closed keyboard.
EDIT:
I'm trying to build an input field that's kind of like posting on Twitter. I need to combine a CircleAvatar, a TextField and a GridView to display the user's avatar, his post and a few images. Like on Twitter, I want the whole thing to scroll, not only the TextField - both while typing and while reviewing what the user typed or uploaded. Besides, the (multiline) TextField should scroll while typing in the visible area (keeping the open or closed keyboard in mind), so the user can see what he's typing.
Even though the Flutter TextField autoscrolls now, I can't get this whole cluster working. Any idea?
Autoscroll has been supported natively in the textfield widget since I believe the beginning of June (2018) - I think this is the commit that added it. You may need to update to the most recent flutter build for it to work, but that is included in version 5.5.
This simplifies matters a bit - this should be all you need to do to get it to work as you want:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Example"),
),
body: new Container(
padding: new EdgeInsets.all(7.0),
child: new TextField(
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: new InputDecoration.collapsed(
hintText: 'Please enter a lot of text',
),
),
),
),
);
}
}
EDIT: To answer the OP's edited question - wanting to have other elements within the same scrolling pane as the textview, I've made this:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Example"),
),
body: new SingleChildScrollView(
child: new Container(
padding: new EdgeInsets.all(7.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new CircleAvatar(
backgroundColor: Colors.blue,
child: new Text("AB"),
),
new Expanded(
child: new Column(
children: [
new TextField(
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: new InputDecoration.collapsed(
hintText: 'Please enter a lot of text',
),
),
new Container(
height: 300.0,
width: 100.0,
color: Colors.green,
),
],
),
),
],
),
),
),
),
);
}
}
By using a SingleChildScrollView, we're still allowing the children to set the size of the viewport (as opposed to a MultiChildLayoutDelegate etc which has to have that size set). The textview grows as big as it needs to be, but doesn't scroll itself as its height is not constrained. The Expanded is needed within the row to make sure that the right side (with the text & pictures) is as large as it can be horizontally.