Search Term not updating from TextField to Firestore StreamBuilder Call - flutter

I am trying to implement a place where users in my app can search my Firestore Database.
When The user enters a search term into the textField:
return Scaffold(
appBar: AppBar(
title: TextField(
textInputAction: TextInputAction.search,
onSubmitted: (value) {
setState(() {
searchKey = value;
streamQuery = db
.collection('GearLockerItems')
.where('searchTerm', isGreaterThanOrEqualTo: searchKey)
.where('searchTerm', isLessThan: '${searchKey}z')
.snapshots();
});
},
decoration: const InputDecoration(
hintText: 'Search for Gear',
prefixIcon: Icon(Icons.search),
),
),
)
I take the value for the search term and place it into the streamQuery
I then take that streamQuery and put it into the StreamBuilder:
body: StreamBuilder<dynamic>(
stream: streamQuery,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
//widget build for complated call
print('Inside stream $searchKey');
if (snapshot.hasData) {
return ListView.builder(
AS you can see I put a print statement in there for testing and Inside Stream $searchKey keeps showing null, which is was I default it to. I do not understand why when I enter data into the search box its not updating the searchKey to what ever I type and is keeping it null...

return Scaffold(
appBar: AppBar(
title: TextField(
textInputAction: TextInputAction.search,
onSubmitted: (value) {
searchKey = value;
await db
.collection('GearLockerItems')
.where('searchTerm', isGreaterThanOrEqualTo: searchKey)
.where('searchTerm', isLessThan: '${searchKey}z')
.snapshots().then((data) { setstate((){streamQuery = data; });});
},
decoration: const InputDecoration(
hintText: 'Search for Gear',
prefixIcon: Icon(Icons.search),
),
),
)

First fetch the data from firestore and use then function to set state.May it works and don't forget to use await because you wait untill the data is fetched.

Related

How to add search history functionality in flutter app

I have recently add search functionality in flutter app and I want to add search history functionality in my flutter app. I am stuck in this I have no idea that how to add it.
Please give me instuction and also provide suitable code for it .
My code
TextFormField(
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
controller: searchcontroller,
focusNode: fousnode,
onChanged: (query) {
setState(() {
FirebaseFirestore.instance
.collectionGroup("Add_product")
.where("product_name", isGreaterThanOrEqualTo: query)
.where("product_name", isLessThan: query + 'z')
.snapshots();
});
},
decoration: InputDecoration(
hintText: "Search Product",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(11)),
),
),
)
StreamBuilder(
stream:
FirebaseFirestore.instance.collectionGroup("Add_product").snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
List<QueryDocumentSnapshot> data = snapshot.data!.docs;
if (searchcontroller.text.isNotEmpty) {
data = data.where((doc) => doc["product_name"]
.toLowerCase()
.contains(searchcontroller.text.toLowerCase()))
.toList();
}
return GridView.builder(
itemCount:data.length,
itemBuilder: (itemBuilder, index) {
return Container(
child: InkWell(
onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (builder)=>detail(
url: snapshot.data!.docs[index]["url"],
productName: snapshot.data!.docs[index]["product_name"],
productPrice: snapshot.data!.docs[index]["product_price"],
)));
},
child: ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(
snapshot.data!.docs[index]["url"]
),
),
title: Text(snapshot.data!.docs[index]["product_name"])
),
),
),
);
},
);
}),
every time a search is performed you can create a list or a map and save it locally or in the database according to your needs. To then make the data that you are going to recover appear under the search bar, I recommend using existing plugins such as material_floating_search_bar
example with shared preferences (locally):
final prefs = await SharedPreferences.getInstance();
List<String> _currentResearch = prefs.getStringList("searches") ?? [];
_currentResearch.add(searchcontroller.text);
await prefs.setStringList("searches", _ricercheAttuali);

Flutter - How to reset Autocomplete list after fetching data from server?

I have Autocomplete list:
List<CompanyName> companyNames = <CompanyName>[
const CompanyName(name: 'No Data'),
];
And this works, only one item No Data is on the array, but that array is filled by data from the server, and the problem is when you press autocomplete on start you will see the No Data item on the list, after server fetching data, the list will not be updated by data from the server.
My idea is to create a local variable that will be updated by the async call, and that variable should hide autocomplete list before the server responds, or refresh the (re-render) widget after fetching...
Autocomplete<CompanyName>(
optionsBuilder:
(TextEditingValue textEditingValue) {
return companyNames.where((CompanyName companyName) {
return companyName.name.toLowerCase().contains(textEditingValue.text.toLowerCase());
}).toList();
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<CompanyName>
onSelected,
Iterable<CompanyName> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
child: ConstrainedBox(constraints: const BoxConstraints(maxHeight: 280,),
child: SizedBox(width: 280,
height: companyNames.length <= 1 ? 80 : 280,
child: ListView.builder(padding: const EdgeInsets.all(10.0),
itemCount: options.length, itemBuilder: (BuildContext context, int index) { final CompanyName option = options.elementAt(index);
return GestureDetector(
onTap: () { onSelected(option); },
child: ListTile( title: Text(option.name, style: TextStyle(color: isDarkMode ? Colors.white : Colors.black)),
),
);
})))));
},
fieldViewBuilder:
(context,
controller,
focusNode,
onEditingComplete) {
return TextFormField(
controller:
controller,
focusNode:
focusNode,
onEditingComplete:
onEditingComplete,
keyboardType:
TextInputType
.text,
autocorrect:
false,
decoration: InputDecoration(
isDense: true,
hintText: "Company Name",
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(10.0),
),
fillColor: isDarkMode ? const Color(0XFF212124) : Colors.white,
filled: true),
validator: (value) {
if (value ==
null ||
value.isEmpty) {
return 'Please enter company name';
} else {
setState(
() {
client =
value;
});
}
return null;
});
},
onSelected:
(CompanyName
selection) {
setState(() {
brokerCompany =
selection
.name;
});
},
displayStringForOption:
(CompanyName
option) =>
option
.name,
),
What is the best option and where is the best option to put the variable and re-render Autocomplete()?

Flutter - calling setState() before the build

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"))
],
);}

How to implement validation with keyboard done button Flutter, observable, streamBuilder

I'm building an app with Flutter and Bloc like architecture. I'm trying to call submit func with not only login button but also keyboard done button with password; only when email and password are valid.
I could implement login button version with combineLatest, but I'm not sure keyboard version. I need to validate both email and password when keyboard done button pressed before calling submit. I could nest streamBuilder, but I feel it is not good practice.
Is there any way to get the latest value from combineLatest? BehaviorSubject<bool>().value Or any possible advice to implement this.
sample code:
final _emailController = BehaviorSubject<String>();
final _passwordController = BehaviorSubject<String>();
// Add data to stream
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password =>
_passwordController.stream.transform(validatePassword);
Stream<bool> get submitValid =>
Observable.combineLatest2(email, password, (e, p) => true);
// change data
Function(String) get changeEmail => _emailController.sink.add;
Function(String) get changePassword => _passwordController.sink.add;
submit() {
final validEmail = _emailController.value;
final validPassword = _passwordController.value;
print('Email is $validEmail, and password is $validPassword');
}
import 'package:flutter/material.dart';
import '../blocs/bloc.dart';
import '../blocs/provider.dart';
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of(context);
return Container(
margin: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
emailField(bloc),
passwordField(bloc),
Container(
margin: EdgeInsets.only(top: 25.0),
),
submitButton(bloc),
],
),
);
}
Widget emailField(Bloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'ypu#example.com',
labelText: 'Email Address',
errorText: snapshot.error,
),
);
},
);
}
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Password',
labelText: 'Password',
errorText: snapshot.error,
),
textInputAction: TextInputAction.done,
onSubmitted: () {}, // <- here
);
});
}
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: snapshot.hasData ? bloc.submit : null,
);
},
);
}
}
You can wrap Your Password streamBuilder with another streamBilder and onSubmitted call submit method.
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snap) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Password',
labelText: 'Password',
errorText: snapshot.error,
),
textInputAction: TextInputAction.done,
onSubmitted: snap.hasData ? bloc.submit : null,
);
});
});
}

Initial value on TextField with StreamBuilder

I have a TextField rendered with the help of a StreamBuilder, following the BLoC pattern with a sink and stream.
Widget field(SignUpBloc signUpBloc) {
return StreamBuilder(
stream: signUpBloc.outFirstName,
builder: (context, snapshot) {
return TextField(
style: TextStyle(fontSize: 15.0),
onChanged: signUpBloc.inFirstName,
decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 15.0),
errorText: snapshot.error
),
);
},
);
}
My question is how do I set up an initial value? I've tried with the StreamBuilder's initialData property but no text appears in the TextField.
TextEditingController _controller = TextEditingController(); // make a controller,
Widget field(SignUpBloc signUpBloc) {
return StreamBuilder(
stream: signUpBloc.outFirstName,
initialData: YourData, // provide initial data
builder: (context, snapshot) {
_controller.value = TextEditingValue(text: "${snapshot.data}"); // assign value to controller this way
return TextField(
controller: _controller,
style: TextStyle(fontSize: 15.0),
onChanged: signUpBloc.inFirstName,
decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 15.0),
errorText: snapshot.error
),
);
},
);
}
Edit: To put the cursor at the end of the line, you can use
var cursorPos = _controller.selection;
if (cursorPos.start > _controller.text.length) {
cursorPos = TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
}
_controller.selection = cursorPos;
Source
You will have to have a TextEditingController when you need to pass initial values. Using the value.copyWith strategy there is no need to deal with the cursor, and to make the widget cleaner you can pass the text controller as a parameter.
// Stream widget
Widget field(SignUpBloc signUpBloc, TextEditingController _txtController) {
return StreamBuilder(
stream: signUpBloc.outFirstName,
builder: (context, snapshot) {
_txtController.value =
_txtController.value.copyWith(text: snapshot.data);
return TextField(
controller: _txtController,
onChanged: signUpBloc.inFirstName;
decoration: InputDecoration(
errorStyle: TextStyle(fontSize: 15.0),
errorText: snapshot.error
),
});
}