Search bar with isar Database - flutter

hi and hello everyone ,
i am new with flutter and dart
and i am start using isar database
and everything is good and ok
but i have problem with Search
i create function for search
getAll(String search) async* {
final isar = await db;
final query = isar.books
.where()
.filter()
.titleContains(search)
.build();
await for (final results in query.watch(fireImmediately: true)) {
if (results.isNotEmpty) {
yield results;
}
}
}
and i add Search i home screen like this in TextField :
onChanged: (value) {
isarService.getAll(search: value);
}
,
but not work with me i try everthing but no work
soory for my english

Inside your getAllBooks() just remove .build()
Stream<List<Book>> getAllBooks({String? search}) async* {
print(search);
final isar = await db;
final query = isar.books
.where()
.filter()
.titleContains(search ?? '', caseSensitive: false);
await for (final results in query.watch(fireImmediately: true)) {
if (results.isNotEmpty) {
yield results;
}
}
}
Convert your book_list_screen.dart class in StatefulWidget, and use a variable.
String search = ""; //for searching
Inside your TextField's onChanged()
onChanged: (value) {
setState(() {
search = value;
});
},
and inside your Expanded widget use something like this
Expanded(
child: StreamBuilder<List<Book>>(
stream: widget.isarService.getAllBooks(search: search),
builder: (context, snapshot) {
...
}
),

maybe you should wait for the response:
onChanged: (value) async {
await isarService.getAll(search: value);
}

Related

Flutter provider listeners not updating themselves when sqflite database data changes

I created a local database using flutter sqflite. And I want to listen to a length of a list of tasks on that database and update the total count of the tasks, when I add something or remove from that list. But when I call provider.of(context) thing, it doesn't update themselves, means it doesn't listen. I used a stream to grab the database data and show in the UI.
Here is the database class I created:
class TaskDatabase with ChangeNotifier {
final String dbName = 'db.sqlite';
Database? _db;
List<Task> _tasksList = [];
int _totalTaskCount = 0;
final _streamController = StreamController<List<Task>>.broadcast();
Stream<List<Task>> all() =>
_streamController.stream.map((tasks) => tasks..sort());
int get totalTasksCount {
return _totalTaskCount;
}
Future<bool> close() async {
final db = _db;
if (db == null) {
return false;
}
await db.close();
return true;
}
Future<bool> open() async {
if (_db != null) {
return true;
}
final directory = await getApplicationDocumentsDirectory();
final path = '${directory.path}/$dbName';
try {
final db = await openDatabase(path);
_db = db;
//creating the database table using sqflite
const createTable = '''CREATE TABLE IF NOT EXISTS "TABLEOFTASKS" (
"id" INTEGER NOT NULL,
"taskTitle" TEXT,
"isDone" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT));''';
await db.execute(createTable);
// read all existing task objects from the db
_tasksList = await _fetchTasks();
_streamController.add(_tasksList);
return true;
} catch (e) {
// print('error = $e');
return false;
}
}
// Creating a new task and save to the database:
// other CRUD functions are not added here:)
Future<bool> create(String taskTitle) async {
final db = _db;
if (db == null) {
return false;
}
try {
final id = await db.insert(
'TABLEOFTASKS',
{
'taskTitle': taskTitle,
'isDone': 0,
},
);
final task = Task(
id: id,
taskTitle: taskTitle,
isDone: false,
);
_tasksList.add(task);
_streamController.add(_tasksList);
_totalTaskCount = _tasksList.length;
notifyListeners();
return true;
} catch (e) {
print('error in creating task = $e');
return false;
}
}
}
Here is the widget that I want to listen and update:
final int taskCount = Provider.of<TaskDatabase>(context, listen: true).totalTasksCount;
.
.
.
Text(taskCount.toString()),
I added the provider at the top of the widget tree and there are no errors. Only thing happening is not updating the text widget
I created a streamBuilder and grabbed the list I want as a snapshot. Updating the list length using the provider package did not work. You can find in the DB class in the question to find how I created a stream of Tasks. Firest initialize the Database in init method.
late final TaskDatabase _crudStorage;
#override
void initState() {
_crudStorage = TaskDatabase();
_crudStorage.open();
super.initState();
}
#override
void dispose() {
_crudStorage.close();
super.dispose();
}
....
return Scaffold(
resizeToAvoidBottomInset: false,
drawer: const CustomDrawer(),
body: StreamBuilder(
stream: _crudStorage.all(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
if (snapshot.data == null) {
return const Center(child: Shimmer());
}
final tasksList = snapshot.data as List<Task>; /// The List I want
.
.
.
.
.
.
SomeTextWidget('The length of tasks = ${tasksList.length}'),

The method 'update' was called on null

When I try to update the task values through the edit page, I get the following error
The method 'update' was called on null.
Receiver: null
Tried calling: update(Instance of 'Tasks')
Form key
final GlobalKey<FormState> _formStateKey = GlobalKey<FormState>();
This is one of the fields for entering text
child: TextFormField(
validator: (value) {
if (value.isEmpty || value.trim() == "") {
return 'This field is required';
}
return null;
},
onSaved: (value) {
_taskName = value;
},
controller: TextEditingController(text: widget.name),
),
The code that is triggered when the "edit" button is pressed.
onPressed: (() {
if (_formStateKey.currentState.validate()) {
_formStateKey.currentState.save();
dbHelper
.update(Tasks(widget.id,
_taskDate,
_taskName,
_taskRepeat,
_taskPriority,
_taskScore,
_taskDone))
.then((data) {
setState(() {
goToMainPage();
});
});
}
}),
The update method by which the task is modified.
Future<int> update(Tasks tasks) async {
var dbClient = await db;
return await dbClient.update(
'tasks',
tasks.toMap(),
where: 'id = ?',
whereArgs: [tasks.id],
);
}
Refreshing from the main page works fine.
From my understanding, it seems like dbHelper is null.
Are you initializing dbHelper anywhere because you haven't provided the
code where you do so.
So, to put it simply, you need to put the code below somewhere in your app.
final dbHelper = Tasks(...) // Fill your data in the 3 dots.

Returning the result of a Future in a synchronous call

I'm using the signature pad in the FlutterFormBuilder package to capture a signature (FlutterFormBuilderSignaturePad), upload it to firebase storage and then return the download url to the application for storage in a document in firestore.
The problem im facing is that the upload takes a couple of seconds to complete (possibly longer on poor connection). I'm trying to await the call so i can pass the download url to the database however its ignoring my attempts.
Ive tried :
Chaining my calls using the .then() and .whenComplete() but valueTransformer still returns a blank string.
added async to the "valueTransformer", "onSaved" and "onChange" methods and awaited the calls
moved the logic to save the signature between the three methods above in order to give the uimage time to upload
onChanges fires a lot so i introduced a _processing flag so it didnt save the image multiple times and cause database timeouts. onChange was returning a url given a few seconds however i couldn't guarantee the signature was complete.
So my widget looking like this:
final SignatureController _controller = SignatureController(
penStrokeWidth: 5,
penColor: Colors.red,
exportBackgroundColor: Colors.blue,
);
String _signature;
File _signatureFile;
bool _processing;
return FormBuilderSignaturePad(
name: 'signature',
controller: _controller,
decoration: InputDecoration(labelText: "signature"),
initialValue: _signatureFile?.readAsBytesSync(),
onSaved: (newValue) async {
//called on save just before valueTransformer
await processSignature(newValue, context);
},
valueTransformer: (value) {
//called when the form is saved
return _signature;
},
onChanged: (value) {
//called frequently as the signature changes
if (_controller.isNotEmpty) {
if (_controller.value.length > 19) {
if (!_processing) {
processSignature(value, context).then((value) {
setState(() {
_processing = false;
});
});
}
}
}
},
)
My future for processing the upload and setting the state
Future<void> processSignature(dynamic signature, BuildContext context) async {
setState(() {
_processing = true;
});
var bytes = await _controller.toPngBytes();
final documentDirectory = await getApplicationDocumentsDirectory();
final file =
File(join(documentDirectory.path, 'signature${database.uid}.png'));
file.writeAsBytesSync(bytes);
var url = await storage.uploadImage(
context: context,
imageToUpload: file,
title: "signature${database.uid}.png",
requestId: database.currentRequest.id);
setState(() {
_signature = url.imageUrl;
_signatureFile = file;
});
}
UPDATES AFTER CHANGES BELOW
Process Signature:
Future<String> processSignature(
dynamic signature, BuildContext context) async {
var bytes = await _controller.toPngBytes();
final documentDirectory = await getApplicationDocumentsDirectory();
final file =
File(join(documentDirectory.path, 'signature${database.uid}.png'));
file.writeAsBytesSync(bytes);
var url = await storage.uploadImage(
context: context,
imageToUpload: file,
title: "signature${database.uid}.png",
requestId: database.currentRequest.id);
return url.imageUrl;
}
Signature Pad Widget:
return FormBuilderSignaturePad(
name: 'signature',
controller: _controller,
decoration: InputDecoration(labelText: "signature"),
initialValue: _signatureFile?.readAsBytesSync(),
onSaved: (newValue) async {},
valueTransformer: (value) async {
final savedUrl = await processSignature(value, context);
return savedUrl;
},
onChanged: (value) {},
);
Method where im seeing the "Future"
_formKey[_currentStep].currentState.save();
if (_formKey[_currentStep].currentState.validate()) {
//request from the database
var request = firestoreDatabase.currentRequest;
//this should be the url however its returning as
//"Future<String>"
var value = _formKey[_currentStep].currentState.value;
request.questions[_currentStep].result =
jsonEncode(_formKey[_currentStep].currentState.value);
request.questions[_currentStep].completedOn =
Timestamp.fromDate(new DateTime.now());
firestoreDatabase.updateRequest(request).then((value) {
if (_currentStep == _totalSteps - 1) {
//pop the screen
Navigator.pop(context);
} else {
setState(() {
_currentStep++;
});
}
It impossible to return async result in sync call. Future means it completes somewhere in future.
Remove processSignature from onChanged (why send signature each time it modified?) and process it in onSaved. Then you can use async/await to send signature to server and wait for result url.
class _SomeWidgetState extends State<SomeWidget> {
/// Form key
final formKey = GlobalKey<FormState>();
/// Contains signature binary daya
Uint8List signatureValue;
#override
void build(...) {
return Column(
children: [
FormBuilderSignaturePad(
...
onSaved(Uint8List value) async {
signatureValue = value;
},
FlatButton(
child: Text('Submit'),
onPressed: () {
_submit();
}
),
],
);
}
/// Submits form
Future< void> _submit() async {
if (formKey.currentState.validate()) {
formKey.currentState.save(); // calls all `onSaved` for each form widgets
// So at this point you have initialized `signatureValue`
try {
final signatureUrl = await processSignature(signatureValue, context); // save into database
await doSomethingWithUrl(signatureUrl); // insert into document
} on SomeExceptionIfRequired catch (e) {
// Show error if occurred
ScaffoldMessenger.of(context).showSnackbar(...);
}
}
}
}

flutter firestore stream mapping to another stream

I have a menus collection on firestore and I want to perform a map operation on each document and return a new stream. So, instead of the Stream<QuerySnapShop>, I wanted Stream<VendorMenuItem>
Stream<VendorMenuItem> getAllVendorMenuItems(String vendorId) async* {
var collectionReference = fs.collection('restaurants').doc('$vendorId').collection("menus").snapshots();
collectionReference.map((event) {
print("mapping");
event.docs.forEach((element) {
return VendorMenuItem.fromMap(element.data());
});
});
}
and I am calling it within a build method just to test my approach, and I got nothing printed on the console, here is how I called it
#override
Widget build(BuildContext context) {
var fs = Provider.of<FireStoreDatabaseRoute>(context);
fs.getAllVendorMenuItems("ewP3B6XWNyqjM98GYYaq").listen((event) {
print("printing final result");
print(event.name);
});
Any clues? thank you
UPDATE:
I wasn't yielding anything, however the yield keyword didnt help
Stream<VendorMenuItem> getAllVendorMenuItems(String vendorId) async* {
var collectionReference = FirebaseFirestore.instance.collection('restaurants').doc('$vendorId').collection("menus").snapshots();
yield* collectionReference.map((event) => event.docs.map((e) => VendorMenuItem.fromMap(e.data())));
}
This is how you transform stream using the method you use.
Stream<List<VendorMenuItem>> getAllVendorMenuItems(String vendorId) async* {
var collectionReference =
FirebaseFirestore.instance.collection('Files').snapshots();
yield* collectionReference.map(
(event) => event.docs
.map(
(e) => VendorMenuItem.fromMap(e.data()),
)
.toList(), //Added to list to Match the type, other wise dart will throw an error something Like MappedList is not a sub type of List
);
}
This is a second way to achieve the same task using a stream controller.
Stream<List<VendorMenuItem>> getAllVendorMenuItems2(String vendorId) {
StreamController<List<VendorMenuItem>> controller =
StreamController<List<VendorMenuItem>>();
FirebaseFirestore.instance.collection("Files").snapshots().listen((event) {
controller.add(event.docs
.map(
(e) => VendorMenuItem.fromMap(e.data()),
)
.toList() //ToList To Match type with List
);
});
return controller.stream;
}
So the reason why it didn't work was I didnt realize the map function is only a middleware and therefore the async* is not required; here is an alternative to #Taha's solution
(without the use of a stream controller)
Stream<List<VendorMenuItem>> getAllVendorMenuItems(String vendorId) {
var snapshot = fs.collection('restaurants').doc(vendorId).collection('menus').snapshots();
return snapshot.map<List<VendorMenuItem>>((event) {
return event.docs.map((e) {
return VendorMenuItem.fromMap(e.data());
}).toList();
});
}

How to NOT show the current user in a Grid View?

I have a function called getAllUsers() that returns all users from a database. The problem is that I want GridView.builder() to display all the users except the current user, but despite all the research I did, nothing seems to work out.
If i use the if condition like if(snapshot.data.documents[i].data["username"] != currentUserId within itemBuilder:, it returns a blank tile which represents the current user which creates a gap within the grid view. Thus, it makes the grid view look really bad.
I believe this problem could have been solved if I knew how to include the inequality query in the getAllUsers() method. But my understanding is that Firestore has yet to provide this function/argument.
HomeFragment class
Database _database = Database();
Stream _stream;
String currentUserId;
#override
void initState() {
getCurrentUserId();
getAllUsers();
super.initState();
}
getAllUsers() async {
return await _database.getAllUsers().then((val) {
if (mounted)
setState(() => _stream = val);
});
}
getCurrentUserId() async {
FirebaseUser currentUser = await FirebaseAuth.instance.currentUser();
currentUserId = currentUser.uid;
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _stream,
builder: (context, snapshot) {
return snapshot.data == null ? Center(child: CircularProgressIndicator())
: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child:
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
itemCount: snapshot.data.documents.length,
itemBuilder: (context, i) {
return Container(
child: Text(snapshot.data.documents[i].data["username"])
);
}
// etc etc..
Database class
getAllUsers() async {
return await _firestore.collection("users").snapshots();
}
I tried to use this, but _stream2 returns null
Stream _stream, _stream2;
getAllUsers() async {
return await _database.getAllUsers().then((val) {
if (mounted) {
List<String> list;
setState(() {
_stream = val;
_stream2 = _stream.where((snapshot) {
_querySnapshot = snapshot;
for (int i = 0; i < _querySnapshot.documents.length; i++)
list.add(_querySnapshot.documents[i].data["userId"]);
return list.contains(currentUserId) == false;
});
});
}
});
}
I also tried this, it is not working
getAllUsers() async {
Stream<QuerySnapshot> snapshots = await _database.getAllUsers();
_stream = snapshots.map((snapshot) {
snapshot.documents.where((documentSnapshot) {
return documentSnapshot.data["userId"] != currentUserId;
});
});
}
Maybe you can try something like this. You filter the query result:
getAllUsers() async {
final Stream<QuerySnapshot> snapshots = await _firestore.collection("users").snapshots();
return snapshots.map((snapshot) {
final result = snapshot.documents
.map((snapshot) => User.fromMap(snapshot.data)
.where((user) => user.id != currentUser.id)
.toList();
return result;
}
}
If you do not have an User class, you can replace some lines with this. But the result will be a list of Map<String, dynamic> instead of a list of User objects.
return snapshots.map((snapshot) {
final result = snapshot.documents
.map((snapshot) => snapshot.data
.where((user) => user['id'] != currentUser.id)
.toList();
return result;
This solution worked well for me.
firestore.collection('your collection').where('x', isNotEqualTo: auth.currentUser!.uid).snapshots();