I would like to achieve Android like TextField validation in Flutter.
I tried the TextField docs of Flutter and the InputDecoration has a property of errorText but is displays error at the bottom of textfield. I want to achieve something like below in Flutter
You can use a TextFormField along with a custom Tooltip inside a Stack to achieve this effect. In the decoration property for the TextFormField you can use the suffixIcon property of the InputDecoration class to pass the error icon.
And you can use a bool variable to show/hide the tooltip message when the validation error occurs.
Example code for the TextFormField:
TextFormField(
decoration: InputDecoration(
//Set the different border properties for a custom design
suffixIcon: IconButton(
icon: Icon(Icons.error, color: Colors.red),
onPressed: () {
setState(() {
showTooltip = !showTooltip; //Toggles the tooltip
});
},
),
),
validator: (String value) {
if(value.isEmpty) {
setState(() {
showTooltip = true; //Toggles the tooltip
});
return "";
}
}
);
You can wrap the above code along with your custom tooltip widget code with a Stack to achieve the error tooltip effect.
Below is a quick working example. You can design your own custom tooltip widget and use it in your code.
Example:
class LoginAlternate extends StatefulWidget {
#override
_LoginAlternateState createState() => _LoginAlternateState();
}
class _LoginAlternateState extends State<LoginAlternate> {
GlobalKey<FormState> _formKey = GlobalKey();
bool showTooltip = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blueGrey,
body: Container(
padding: EdgeInsets.symmetric(
horizontal: 100,
vertical: 100
),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
Stack(
alignment: Alignment.topRight,
overflow: Overflow.visible,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderSide: BorderSide.none
),
suffixIcon: IconButton(
icon: Icon(Icons.error, color: Colors.red,),
onPressed: () {
setState(() {
showTooltip = !showTooltip;
});
},
),
hintText: "Password"
),
validator: (value) {
if(value.isEmpty) {
setState(() {
showTooltip = true;
});
return "";
}
},
),
Positioned(
top: 50,
right: 10,
//You can use your own custom tooltip widget over here in place of below Container
child: showTooltip
? Container(
width: 250,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all( color: Colors.red, width: 2.0 ),
borderRadius: BorderRadius.circular(10)
),
padding: EdgeInsets.symmetric(horizontal: 10),
child: Center(
child: Text(
"Your passwords must match and be 6 characters long.",
),
),
) : Container(),
)
],
),
RaisedButton(
child: Text("Validate"),
onPressed: () {
_formKey.currentState.validate();
},
),
],
),
),
),
);
}
}
I think you are talking about ToolTip
You can use this library or Go through Flutter doc
new Tooltip(message: "Hello ToolTip", child: new Text("Press"));
you can use the library super_tooltip #
You can customize this type of error by using overlay widget. Error can be shown with overlay widget and that error icon can be changed using inputDecoration of TextField.
Here is the link for understanding overlay widget--
https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9
Related
I am developing a feature where the user enters a sentence, in the next screen the words of that sentence get shuffled randomly, then the user has to drag the words to a drag target to form the original sentence.
You can get an idea from the screenshots below.
First screen
Second screen
Now the problem I am having is, when dragging the words to the target I can see the DragTarget is calling onWillAccept as I added a print() statement there, if it is doing so then it should call onAccept eventually but it is not doing so. This is why my codes that deal with Bloc are not getting called and the words are not showing up in the target spot.
Code
class SentenceMakeScreen extends StatefulWidget {
String inputSentence;
SentenceMakeScreen(this.inputSentence);
#override
State<SentenceMakeScreen> createState() => _SentenceMakeScreenState();
}
class _SentenceMakeScreenState extends State<SentenceMakeScreen> {
List<String> sentence = [];
List<Widget> wordWidgets = [];
bool isDragSuccessful = false;
final ButtonStyle _buttonStyle = ElevatedButton.styleFrom(
textStyle: TextStyle(fontSize: 20)
);
_getTextWidgets(List<String> sentence) {
for(var i = 0; i < sentence.length; i++){
wordWidgets.add(
Draggable<WordWidget>(
data: WordWidget(sentence[i]),
child: WordWidget(sentence[i]),
feedback: WordWidget(sentence[i]),
childWhenDragging: Container(),
)
);
}
}
_randomlyOrganizeSentence(String inputString) {
sentence = inputString.split(new RegExp(r" "));
sentence.shuffle();
print(sentence);
}
#override
void initState() {
// TODO: implement initState
_randomlyOrganizeSentence(widget.inputSentence);
_getTextWidgets(sentence);
super.initState();
}
#override
Widget build(BuildContext context) {
final _dragDropBloc = DragDropBloc();
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
DragTarget<WordWidget>(
builder: (context, data, rejectedData) {
return Center(
child: this.isDragSuccessful
?
Container(
width: double.maxFinite,
margin: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(width: 1.0, color: Colors.black),
),
),
child: StreamBuilder<List<WordWidget>>(
stream: _dragDropBloc.widgetStream,
initialData: [],
builder: (BuildContext context, AsyncSnapshot<List<WordWidget>> snapshot) {
print("Here ${snapshot.data}");
return Wrap(
direction: Axis.horizontal,
children: [
//correctly ordered words
],
);
},
),
)
:
Container(
width: double.maxFinite,
margin: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(width: 1.0, color: Colors.black),
),
),
child: Text("Drag here")
),
);
},
onWillAccept: (data) {
print("true");
return true;
},
onAccept: (data) {
print(data.toString());
_dragDropBloc.dragDropEventSink.add(
DropEvent(WordWidget(data.toString()))
);
setState(() {
this.isDragSuccessful = true;
//draggedWords.add(data.toString());
});
},
),
Wrap(
direction: Axis.horizontal,
children: wordWidgets
),
Container(
child: ElevatedButton(
style: _buttonStyle,
onPressed: () {
},
child: Text("Check"),
),
),
],
),
),
);
}
}
WordWidget
import 'package:flutter/material.dart';
class WordWidget extends StatelessWidget {
final String word;
const WordWidget(this.word);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red[900],
border: Border.all(
width: 4,
color: Colors.black
),
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: EdgeInsets.all(5),
child: Text(
word,
style: TextStyle(
color: Colors.white
),
)
),
);
}
}
I tried adding the type of data I am passing from Draggable to DragTarget, this is what was advised here. It did not work.
I was also getting the same error earlier today. I then upgraded my flutter to the latest version and wrote the DragTarget code again from scratch. I don't know what worked for me but you can try doing the same.
I made a list of class OriginDestination, i.e. _allCities in my file. I then assigned all values in filteredCities and cities in initState.
Then I made a function runFilter which would take keyword from TextField and filter the results accordingly and save them to resultCities. Then I am using resultCities to display the information in ListView.builder. But the problem is, the list is not filtering according to the keyword i am searching.
Also, it would be appreciated if you can suggest a better way of using parameter cities, i.e. I don't think that passing the cities as parameter through state's constructor is a good practice.
Here is the code -
import 'package:flutter/material.dart';
import 'package:passenger_flutter_app/models/new_city.dart';
import 'package:passenger_flutter_app/models/origin_destination.dart';
import 'package:passenger_flutter_app/utils/colors.dart';
class SelectionScreen extends StatefulWidget {
List<OriginDestination>? cities;
SelectionScreen({this.cities});
#override
_SelectionScreenState createState() => _SelectionScreenState(cities);
}
class _SelectionScreenState extends State<SelectionScreen> {
final List<OriginDestination>? _allCities;
_SelectionScreenState(this._allCities);
bool originSelected=false;
List<OriginDestination>? resultCities = [];
List<OriginDestination>? filteredCities = [];
void getCitiesFromResponse() {
/*for(var city in _allCities!) {
cities!.add(city.origin!);
}*/
filteredCities=_allCities;
resultCities=_allCities;
}
#override
initState() {
// at the beginning, all users are shown
getCitiesFromResponse();
super.initState();
}
void _runFilter(String enteredKeyword) {
if (enteredKeyword.isEmpty) {
// if the search field is empty or only contains white-space, we'll display all users
filteredCities = _allCities;
} else {
filteredCities = _allCities!
.where((city) =>
city.origin!.name!.toLowerCase().contains(enteredKeyword.toLowerCase()))
.toList();
// we use the toLowerCase() method to make it case-insensitive
}
#override
void setState() {
resultCities=filteredCities;
}
}
#override
Widget build(BuildContext context) {
var originSelected;
return SafeArea(
child: Scaffold(
backgroundColor: const Color(0xffEEEDEF),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Column(
children: [
Container(
width: MediaQuery.of(context).size.width*0.8,
),
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.orange,
onPressed: () {
Navigator.pop(context);
},
),
Column(
children: [
originSelected==true ? Container(
child: Text(''),
) :
Container(
width: MediaQuery.of(context).size.width * 0.85,
height: 50.0,
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
const BorderRadius.all(Radius.circular(5.0)),
border: Border.all(color: colorAccent)),
child: TextField(
decoration: InputDecoration(
hintText: "Enter Origin",
border: InputBorder.none,
contentPadding: const EdgeInsets.only(left: 10.0),
hintStyle: TextStyle(
fontSize: 15.0,
color: Colors.grey[500],
),
),
onChanged: (value) {
_runFilter(value);
},
),
),
],
),
],
),
],
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
left: MediaQuery.of(context).size.width * 0.04, top: 3.0),
child: Text(
'Popular Searches:',
style: TextStyle(
color: popUpLightTextColor,
fontSize: MediaQuery.of(context).size.width * 0.035),
),
),
),
Expanded(
child: ListView.builder(
itemCount: resultCities!.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.only(
left: 18.0, top: index==0 ? 29.0 : 15.0, bottom: 15.0),
child: InkWell(
onTap: () {
print(resultCities?[index].origin!.name);
/*setState(() {
widget.city=filteredCities[index]['city'];
print("Changed to - ");
//print(widget.city);
Navigator.pop(context);
});*/
},
child: Text(
resultCities?[index].origin!.name??"No name",
style: const TextStyle(
color: darkText,
fontSize: 15.0,
fontWeight: FontWeight.normal,
),
),
),
);
},
),
),
],
),
),
);
}
}
seems like you defined the setState function instead of calling it, so instead of:
#override
void setState() {
resultCities=filteredCities;
}
write:
setState(() {
resultCities=filteredCities;
});
Why are you overriding the setState. You also pass the call back as argument in the setState.
You should be call setState on a trigger, like a gesture or button:
setState(() => resultCities = filteredCities);
I'm trying to create a smooth animation using the AnimatedCrossFade widget but I noticed 2 problems:
Button dimension changes and expands during animation.
The desired outcome is that both buttons match the parent's width and that the color and text changes transition smoothly, but here's what happens.
Without AnimatedCrossFade, Button 1 looks like this:
If I wrap it inside an AnimatedCrossFade widget, Button 1 looks like this:
While the transition is happening, It looks like this:
TextField with InputDecoration stroke becomes thinner
I have multiple TextField widgets that I want to use in the page but some need to be animated in. The problem is that when I put a TextField inside an AnimatedCrossFade widget, the bottom line becomes thinner making the layout look horrible. Here's a comparison of how a TextField looks inside an AnimatedCrossFade (top) and outside of it (bottom).
This is how the layout looks after animation.
But it should look like this.
This code sample should be enough to recreate what I'm trying to explain.
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isExpanded = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
padding: EdgeInsets.symmetric(horizontal: 60, vertical: 60),
children: [
ElevatedButton(
child: Text(_isExpanded ? "Collapse" : "Expand"),
onPressed: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
),
AnimatedCrossFade(
crossFadeState: _isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(seconds: 1),
firstChild: TextField(
decoration: InputDecoration(
hintText: "Text",
),
),
secondChild: SizedBox.shrink(),
),
TextField(
decoration: InputDecoration(
hintText: "Text",
),
),
AnimatedCrossFade(
crossFadeState: !_isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(seconds: 1),
firstChild: ElevatedButton(
child: Text("Button 1"),
onPressed: () {},
),
secondChild: ElevatedButton(
child: Text("Button 2"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {},
),
),
],
),
);
}
}
Hope this is what you want?
I think to handle this case, you need to use layoutBuilder of AnimatedCrossFade
if you click on layoutBuilder you can find details.
Updated
wrap with padding to solve TextFiledFormat, for more you can use decoration.
To use max width i used like this
AnimatedCrossFade(
crossFadeState: _isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(seconds: 1),
firstChild: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: TextField(
key: ValueKey("text1"),
decoration: InputDecoration(
hintText: "Text1",
),
),
),
secondChild: SizedBox.shrink(),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: TextField(
key: ValueKey("text2"),
decoration: InputDecoration(
hintText: "Text",
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: AnimatedCrossFade(
crossFadeState: !_isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(seconds: 1),
alignment: Alignment.center,
layoutBuilder:
(topChild, topChildKey, bottomChild, bottomChildKey) {
return topChild;
},
secondChild: ElevatedButton(
child: Text("Button 2"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {},
),
firstChild: ElevatedButton(
child: Text("Button 1"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {},
),
),
),
To get default size of button wrapped with Center inside layoutBuilder
layoutBuilder:
(topChild, topChildKey, bottomChild, bottomChildKey) {
return topChild;
},
I'm stuck with making a scrollable list like Google Task app when you reach end of the list if any task is completed it shown in another list with custom header as you can see here, I'm using sliver
Widget showTaskList() {
final todos = Hive.box('todos');
return ValueListenableBuilder(
valueListenable: Hive.box('todos').listenable(),
builder: (context, todoData, _) {
int dataLen = todos.length;
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
floating: true,
expandedHeight: 100,
flexibleSpace: Container(
padding: EdgeInsets.only(
left: MediaQuery.of(context).size.width / 10,
top: MediaQuery.of(context).size.height / 17),
height: 100,
color: Colors.white,
child: Text(
'My Task',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w600),
),
),
),
SliverList(
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
final todoData = todos.getAt(index);
Map todoJson = jsonDecode(todoData);
final data = Todo.fromJson(todoJson);
return MaterialButton(
padding: EdgeInsets.zero,
onPressed: () {},
child: Container(
color: Colors.white,
child: ListTile(
leading: IconButton(
icon: data.done
? Icon(
Icons.done,
color: Colors.red,
)
: Icon(
Icons.done,
),
onPressed: () {
final todoData = Todo(
details: data.details,
title: data.title,
done: data.done ? false : true);
updataTodo(todoData, index);
}),
title: Text(
data.title,
style: TextStyle(
decoration: data.done
? TextDecoration.lineThrough
: TextDecoration.none),
),
subtitle: Text(data.details),
trailing: IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {
todos.deleteAt(index);
}),
),
),
);
}, childCount: dataLen),
),
],
);
});
}
ShowTaskList is called on
Scaffold(
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: showTaskList()
),
]),
),
I tried OffStageSliver to make an widget disappear if no complete todo is present but that did not work and also can not use any other widget on CustomScrollView because that conflict with viewport because it only accept slivers widget.
Here what i have achieved so far
You can try use ScrollController put it on CustomScrollView and listen to it's controller in initState like this :
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// If it reach end do something here...
}
});
}
I suggest you make bool variable to show your widget, initialize it with false and then after it reach end of controller call setState and make your variable true, which you can't call setState in initState so you have to make another function to make it work like this:
reachEnd() {
setState(() {
end = true;
});
}
Put that function in initState. And make condition based on your bool variabel in your widget
if(end) _yourWidget()
Just like that. I hope you can understand and hopefully this is working the way you want.
I am using FirebaseMessaging to push notifications on my app.
So I can handle these notification with this code :
firebaseMessaging.configure(
onLaunch: (Map<String, dynamic> msg) {
print("onLaunch called");
}, onResume: (Map<String, dynamic> msg) {
print("onResume called");
}, onMessage: (Map<String, dynamic> msg) {
print("onMessage called : " + msg.toString());
});
When I receive a notification, I want to display this little '1' on my icon in my appbar
My problem is : I don't know how to change my bell icon dynamically on my appbar for all pages (and I can't call setState in my appbar)
I think is pretty simple to solve your problem you only need to use a Stateful class and a custom icon as below snippet:
Widget myAppBarIcon(){
return Container(
width: 30,
height: 30,
child: Stack(
children: [
Icon(
Icons.notifications,
color: Colors.black,
size: 30,
),
Container(
width: 30,
height: 30,
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: 5),
child: Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0xffc32c37),
border: Border.all(color: Colors.white, width: 1)),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: Center(
child: Text(
_counter.toString(),
style: TextStyle(fontSize: 10),
),
),
),
),
),
],
),
);
}
and later you can include this icon on your app bar(leading or action). As you can see the Text value change with any touch I used as base the example code when you start a new Flutter project it contains a method to count how many times you touch the floating button and change the state:
void _incrementCounter() {
setState(() {
_counter++;
});
}
I hope this helps you
this is my example
Basic Idea behind the notification badge
Using Stack and Positioned widgets we can stack the Text widget over the
IconButton to show the notification badge.
appBar: AppBar(
leading: IconButton(
icon: Icon(
_backIcon(),
color: Colors.black,
),
alignment: Alignment.centerLeft,
tooltip: 'Back',
onPressed: () {
},
),
title: Text(
"Title",
style: TextStyle(
color: Colors.black,
),
),
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
tooltip: 'Search',
icon: const Icon(
Icons.search,
color: Colors.black,
),
onPressed: _toggle,
),
new Padding(
padding: const EdgeInsets.all(10.0),
child: new Container(
height: 150.0,
width: 30.0,
child: new GestureDetector(
onTap: () {
},
child: Stack(
children: <Widget>[
new IconButton(
icon: new Icon(
Icons.shopping_cart,
color: Colors.black,
),
onPressed: () {
}),
ItemCount == 0
? new Container()
: new Positioned(
child: new Stack(
children: <Widget>[
new Icon(Icons.brightness_1,
size: 20.0, color: Colors.orange.shade500),
new Positioned(
top: 4.0,
right: 5.0,
child: new Center(
child: new Text(
ItemCount.toString(),
style: new TextStyle(
color: Colors.white,
fontSize: 11.0,
fontWeight: FontWeight.w500),
),
)),
],
)),
],
),
),
),
)
],
),
You have to create a custom drawable and set it as the Appbar icon and you have to paint the number as text in the custom drawable. This is already done for you in the following link.
How to make an icon in the action bar with the number of notification?
you can just create variable of type IconData and change it's value. you will get more idea about that after gone through below example.
import 'package:flutter/material.dart';
void main() => runApp(MyHome());
class MyHome extends StatefulWidget {
#override
_MyHomeState createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
IconData _iconData= Icons.notifications;
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Color(0xffFF5555),
),
home: Scaffold(
appBar: new AppBar(
title: new Text("Title"),
actions: <Widget>[
Icon(_iconData)
],
),
body: Center(
child: new Text("Demo")
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.check_circle_outline),
onPressed: (){
if(_iconData == Icons.notifications){
setState(() {
_iconData = Icons.notifications_active;
});
}else{
setState(() {
_iconData = Icons.notifications;
});
}
}
),
),
);
}
}