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),
),
),
Related
Recently implemented a tagForm widget at "+" button press, I want to delete those widgets now at "delete" button press, but right now, even when I press the "delete" button, nothing happens.
How can I solve this?
Any help appreciated!
code:
import 'package:flutter/material.dart';
import '../database/firestoreHandler.dart';
import '../models/todo2.dart';
import '../widgets/dialogs.dart';
class TodoEdit extends StatefulWidget {
String? doctitle;
String? doctdescription;
String? docimage;
String? docid;
List? doctags;
TodoEdit({Key? key, this.doctitle, this.doctdescription, this.docimage, this.docid,this.doctags}) : super(key: key);
#override
_TodoEditState createState() => _TodoEditState();
}
class _TodoEditState extends State<TodoEdit> {
final _formKey = GlobalKey<FormState>();
final tcontroller = TextEditingController();
final dcontroller = TextEditingController();
final icontroller = TextEditingController();
var textEditingControllers = <TextEditingController>[];
//-----------------the list where the form is stored----------
var textformFields = <Widget>[];
void _addformWidget(controller) {
setState(() {
textformFields.add(tagForm(controller));
});
}
//------------------------------------------------------------------------
Widget tagForm(controller){
return TextFormField(
controller: controller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Tag",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
suffixIcon: IconButton(
icon:Icon(Icons.delete, color: Colors.white,),
//--------------------- doesn't work?-------------------
onPressed: (){
setState(() {
textformFields.remove(tagForm(controller));
});
},
--------------------------------------------------------------
)
),
);
}
//-----------------------------------------------------------
#override
void initState() {
super.initState();
tcontroller.text = widget.doctitle.toString();
dcontroller.text = widget.doctdescription.toString();
icontroller.text = widget.docimage.toString();
widget.doctags?.forEach((element) {
var textEditingController = new TextEditingController(text: element);
textEditingControllers.add(textEditingController);
//return textformFields.add(tagForm(textEditingController)
return _addformWidget(textEditingController);
//);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[900],
appBar: AppBar(
actions: [
IconButton(onPressed: (){
showDialog(
barrierDismissible: false,
context: context,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text('Delete TODO'),
actions: [
TextButton(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: Text('Delete'),
onPressed: () {
deleteData(widget.docid.toString(), context);
setState(() {
showSnackBar(context, 'todo "${widget.doctitle}" successfully deleted!');
});
},
),
],
);
},
);
},
icon: Icon(Icons.delete))
],
backgroundColor: Colors.grey[900],
title: Text("${widget.doctitle}"),
),
body: Container(
child: SafeArea(
child: Form(
key: _formKey,
child: Column(
children: [
SizedBox(height: 10),
TextFormField(
controller: tcontroller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Title",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
),
),
SizedBox(height: 10),
TextFormField(
controller: dcontroller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Description",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
),
),
SizedBox(height: 10),
TextFormField(
controller: icontroller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Image url",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
),
),
SizedBox(height: 10),
Row(children: [
Text("Tags:", style:TextStyle(color: Colors.white)),
IconButton(onPressed: (){
var textEditingController = new TextEditingController(text: "tag");
textEditingControllers.add(textEditingController);
_addformWidget(textEditingController);
print(textformFields.length);
},
icon: Icon(Icons.add,color: Colors.white,),
)
],),
/*SingleChildScrollView(
child: new Column(
children: textformFields,
)
),*/
Expanded(
child: SizedBox(
height: 200.0,
child: ListView.builder(
itemCount: textformFields.length,
itemBuilder: (context,index) {
return textformFields[index];
}),
)
),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
List<String> test = [];
textEditingControllers.forEach((element) {
test.add(element.text);
});
if(tcontroller == '' && dcontroller == '' && icontroller == ''){
print("not valid");
}else{
var todo = Todo2(
title: tcontroller.text,
description: dcontroller.text,
image: icontroller.text,
tags: test,
);
updateData(todo, widget.docid.toString(),context);
setState(() {
showSnackBar(context, 'todo ${widget.doctitle} successfully updated!');
});
}
},
child: Icon(Icons.update),
),
);
}
}
You can't remove anything from the list with objects from tagForm(controller), because these objects are newly created and therefore not the same as in the list (as long as the == operator is not overwritten)
If you still want to have the widgets in a list instead of just storing the controllers and without having to change much, you could remove the widgets like this:
onPressed: (){
setState(() {
controller.dispose();
textEditingControllers.remove(controller);
textformFields.removeWhere((w) => w.controller = controller));
});
},
and change the type of your List: var textformFields = <TextFormField>[]; and of the method TextFormField tagForm(controller).
In general, you can of course optimize the state management, but with this solution it should work for now.
Dont't store Widget, it is bad way. Insteads store there property, render by List then remove by index when you need.
ps: some code syntax can wrong, i write this on browser.
class _TodoEditState extends State<TodoEdit> {
...
var textformFields = <String>[];
...
void _addformWidget([String? initValue]) {
setState(() => textformFields.add(initValue ?? ""));
}
...
Widget tagForm(String value, void Function(String) onChange, void Function() onRemove){
var openEditor = () {
// Open dialog with text field to edit from [value] call onChange with
// new value
OpenDialog().then((newvalue) {
if(newvalue != null) onChange(newvalue);
}
};
var delete = () {
// Open confirm dialog then remove
OpenConfirmDialog("your message").then((continue) {
if(continue) onRemove();
});
};
return InkWell(
onTap: openEditor,
child: Text(value), // render your tag value
);
}
...
#override
void initState() {
...
textformFields = List.filled(widget.doctags ?? 0, ""); // or List.generate/map if you want replace by own value.
}
...
#override
Widget build(BuildContext context) {
...
ListView.builder(
itemCount: textformFields.length,
itemBuilder: (context,index) => tagForm(
textformFields[index],
(newvalue) => setState(() => textformFields[index] = newvalue),
() => setState(() => textformFields = textformFields..removeAt(index));,
),
),
...
);
}
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 have the following code that initially displays a blank page, but I'd like an rAlert dialog to display automatically. Once the user clicks 'Request' or 'Cancel', some text will be displayed on the screen.
But I can't get the code to run that displays the Alert. I had it working by showing a button and clicking the button, but i need the Alert to display automatically when the page is displayed. I tried putting it in the initState. I didn't get any errors, but it didn't work either.
Anyone know what I need to do? Thanks?
import 'package:flutter/material.dart';
import 'package:rflutter_alert/rflutter_alert.dart';
import 'dart:async';
import 'package:rostermeon/cwidgets/general_widgets.dart';
import 'package:rostermeon/rmo_constants.dart';
class ForgotPassword extends StatefulWidget {
static const String id = 'forgot_password_screen';
#override
_ForgotPasswordState createState() => _ForgotPasswordState();
}
class _ForgotPasswordState extends State<ForgotPassword> {
StreamController<bool> _events;
#override
initState() {
super.initState();
_events = new StreamController<bool>();
doRequest(context: context);
}
Future<bool> doSaveRequest({String pReason}) async {
await Future.delayed(const Duration(seconds: 3), () {});
return false;
}
Future<bool> doRequest({context}) {
String _reason = '';
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
TextEditingController reasonController = TextEditingController();
TextStyle _style = TextStyle(fontFamily: 'Montserrat', fontSize: 18.0, fontWeight: FontWeight.normal);
InputDecoration _textFormFieldDecoration({String hintText, double padding}) => InputDecoration(
//contentPadding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 8.0),
contentPadding: EdgeInsets.all(padding),
isDense: true,
hintText: hintText,
hintStyle: TextStyle(color: kHintText),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
);
return Alert(
context: context,
title: 'Request New Password',
content: StreamBuilder<bool>(
initialData: false,
stream: _events.stream,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
print(" ${snapshot.data.toString()}");
return snapshot.data
? CircularProgressIndicator()
: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 20.0),
Text('Email', textAlign: TextAlign.left, style: _style),
SizedBox(height: 10.0),
TextFormField(
validator: (value) {
if (value.isEmpty) {
return "please enter email";
}
return null;
},
onSaved: (value) {
_reason = value;
},
decoration: _textFormFieldDecoration(
hintText: 'your email address',
padding: 8.0,
),
controller: reasonController,
),
SizedBox(height: 10.0),
],
),
);
}),
buttons: [
DialogButton(
child: Text('Request', style: TextStyle(color: Colors.white, fontSize: 20)),
color: kMainColor,
onPressed: () async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
print(_reason);
_events.add(true);
var saved = await doSaveRequest(pReason: _reason);
if (saved) {
Navigator.pop(context, false);
} else {
_events.add(false);
}
Navigator.of(context).pop();
// Navigator.pop(context, false);
}
},
),
DialogButton(
child: Text('Cancel', style: TextStyle(color: Colors.white, fontSize: 20)),
color: kMainColor,
onPressed: () {
Navigator.pop(context, false);
},
),
],
).show();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: rmoAppBar(subText: 'Request New Password', hideBackButton: false),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[],
),
),
);
}
}
Dialogs/Alerts need the buildContext in order to work, You can't have the buildContext before build() method is called, that's why you can't just call it in initstate() before the build is called.
To make it work use addPostFrameCallback to make sure it delays until widget is built:
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourMethod(context));
}
https://www.didierboelens.com/2019/04/addpostframecallback/
The app I am building has comments and posts and I am wondering how I would be able to implement tagging in comments and posts similar to Instagram? I don't think there are any packages for this last I checked. Would I just implement it like a search bar for both in comment and post tagging? but then I can't use a search delegate for this because then it'll bring me to another screen, the search screen, I want it to be similar to Facebook and Instagram's search. Any ideas?
Modify it as your requirement !
TextEditingController ctrl;
List<String> users = ['Naveen', 'Ram', 'Satish', 'Some Other'],
words = [];
String str = '';
List<String> coments=[];
#override
void initState() {
super.initState();
ctrl = TextEditingController();
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
controller: ctrl,
decoration: InputDecoration(
hintText: 'Comment',
hintStyle: TextStyle(color: Colors.black),
suffixIcon: IconButton(
icon: Icon(Icons.send, color: Colors.blue),
onPressed: () {
if(ctrl.text.isNotEmpty)
setState((){
coments.add(ctrl.text);
});
}),
),
style: TextStyle(
color: Colors.black,
),
onChanged: (val) {
setState(() {
words = val.split(' ');
str = words.length > 0 &&
words[words.length - 1].startsWith('#')
? words[words.length - 1]
: '';
});
}),
str.length > 1
? ListView(
shrinkWrap: true,
children: users.map((s){
if(('#' + s).contains(str))
return
ListTile(
title:Text(s,style: TextStyle(color: Colors.black),),
onTap:(){
String tmp = str.substring(1,str.length);
setState((){
str ='';
ctrl.text += s.substring(s.indexOf(tmp)+tmp.length,s.length).replaceAll(' ','_');
});
});
else return SizedBox();
}).toList()
):SizedBox(),
SizedBox(height:25),
coments.length>0 ?
ListView.builder(
shrinkWrap:true,
itemCount:coments.length,
itemBuilder:(con,ind){
return Text.rich(
TextSpan(
text:'',
children:coments[ind].split(' ').map((w){
return w.startsWith('#')&&w.length>1 ?
TextSpan(
text:' '+w,
style: TextStyle(color: Colors.blue),
recognizer:new TapGestureRecognizer()..onTap=()=>showProfile(w),
): TextSpan(text:' '+w,style: TextStyle(color: Colors.black));
}).toList()
),
);
},
):SizedBox()
]));
}
showProfile(String s){
showDialog(
context:context,
builder:(con)=>
AlertDialog(
title:Text('Profile of $s'),
content:Text('Show the user profile !')
));
}
I have a dialog box, which has a TextField(), and a Button. On press of it, I'm calling my API, but before that, I need to put a check on the text. Since everything works fine, I was wondering to find some way out to show the error text inside the dialog box only.
After all the research and work, the dialog box is not recreating itself hence no message is showing even if the boolean case becomes true.
CODE:
class ListViewElements extends StatefulWidget {
#override
_ListViewElementsState createState() => _ListViewElementsState();
}
class _ListViewElementsState extends State<ListViewElements> {
bool isError = false;
bool errorText = false;
final TextEditingController _code = new TextEditingController();
onSubmit(BuildContext context){
if(this._code.text.isEmpty){
setState(
(){
isError = true;
errorText = 'Empty space';
}
);
}
}
void registerDialog(BuildContext context){
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return AlertDialog(
content: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(40.0, 0.0, 40.0, 10.0),
child: TextField(
textAlign: TextAlign.center,
controller: this._code,
maxLength: 4,
textInputAction: TextInputAction.done,
cursorColor: Theme.of(context).primaryColor,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Enter here',
counterText: '',
)
)
),
isError == true ? Text(this.erroText) : Container()
]
),
actions: <Widget>[
FlatButton(
child: new Text("Done", style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 17.0)),
onPressed: () {onSubmit(context);}
)
]
);
}
);
I thought of showing a SnackBar(), but for dialog box, Scaffold.of() is not working out, since the builder is creating the new instance of the box, hence cannot recall. Any help would be appreciated. Thanks :)