Flappy search bar : suggestions not updated after Firebase modifications - firebase

I am quite new to flutter, and I have some issues using this package : https://pub.dev/packages/flappy_search_bar
I am using it with suggestions (made when nothing is written in the search bar), and I have different issues with it, due to the fact that the suggestion list does not update when changes append on Firebase.
Here is my code (in a stateful widget):
SearchBarController _searchBarController=SearchBarController();
List<DocumentSnapshot> documents =[];
List<LibraryBook> books = [];
#override
void initState() {
super.initState();
FireHelper().libraryBooksFrom(widget.user.uid).listen((event) {
setState(() {
books=getLibraryBooks(documents);
});
});
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FireHelper().libraryBooksFrom(widget.user.uid),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(snapshot.hasData) {
documents = snapshot.data.docs;
books=getLibraryBooks(documents);
return Scaffold(
backgroundColor: white,
floatingActionButton: FloatingActionButton(
onPressed: () => setState((){
AlertHelper().addBookToLibrary(context);
}),
child: Icon(Icons.add),
backgroundColor: pointer,
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
body: Column(
children: <Widget>[
SafeArea(
bottom: false,
child: Row(
children: <Widget>[
BackButton(color: Colors.black),
MyText(" Ma bibliothèque", color: baseAccent)
],
)),
Expanded(child: SearchBar(
searchBarPadding: EdgeInsets.symmetric(horizontal: 10),
onSearch: (inputText) => searchBooks(inputText),
suggestions: books,
minimumChars: 2,
crossAxisCount: 2,
onCancelled: () => searchBooks(null),
crossAxisSpacing: 0,
onError: (error) => ErrorWidget(error),
searchBarController: _searchBarController,
hintText: "Chercher un livre...",
cancellationWidget: Text("Annuler"),
emptyWidget: Text("Aucune correspondance"),
onItemFound: (item, int index) {
if(item is LibraryBook){
return BookLibraryTile(item, null);
} else {
return Text("Aucune correspondance");
}
}
)
)
],
),
);
} else {
return LoadingCenter();
}
});
}
When I have some changes on Firebase, the list List<LibraryBook> books is well updated, but the suggestions of the searchBar does not follow this update...
Any idea ?
This is what the screen looks like
EDIT :
first issue when cancelling a search
second issue when deleting an item
third issue when adding a new item
(this one does not append every time... i don't know why)

What you need is a state management solution. I suggest checking out the Provider package. You need a single model for the books that is shared between widgets.
Also, check out this Flutter article on state management if you are not familiar.

Related

How to invisible widget when there is no data in firebase?

i am new to flutter, i'm trying to invisible button when there is no data in Firebase.
To get data i'm using StreamBuilder, if snapshot.data!.docs is null i want to invisible CustomButton which is outside of StreamBuilder.
StreamBuilder:
bool _isVisible = true; //variable
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
key: _scaffoldKey,
appBar: _appBar(context),
body: CommonRefreshIndicator(
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.doc(_currentUser!.uid)
.collection('favourites')
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const CustomProgressIndicator();
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const CustomProgressIndicator();
}
final data = snapshot.data!.docs;
allData = snapshot.data!.docs;
if (data.isNotEmpty) { //update based on data
_isVisible = true;
} else {
_isVisible = false;
}
return data.isNotEmpty
? _favItemListView(data)
: const Center(
child: Text('No data found'),
);
},
),
),
bottomNavigationBar: _addAllToFavButton(size),
);
}
CustomButton:
Padding _addAllToFavButton(Size size) => Padding(
padding: kSymmetricPaddingHor,
child: Visibility(
visible: _isVisible,
child: CustomButton(
label: 'Add all to my cart',
onPressed: () {},
),
),
);
i have tried with Visibility widget and its work but whenever i'm deleting all data CustomButton is still visible, to invisivle CustomButton every time need to do hot reload.
NOTE: setState is also not working its giving me error.
if any one can help me! Thanks.
If you want to hide your CustomButton when there is no data you can try this:
Put your _favItemListView(data) & _addAllToFavButton(size) inside Stack and give Positioned to your CustomButton with its bottom : 1 property.
StreamBuilder
return data.isNotEmpty
? Stack(
children: [
_favItemListView(data),
_addAllToFavButton(size),
],
)
: const Center(
child: Text('No data found'),
);
CustomButton:
Positioned _addAllToFavButton(Size size) => Positioned(
width: size.width,
bottom: 1, //bottom property
child: Padding(
padding: kSymmetricPaddingHor,
child: CustomButton(
label: 'Add all to my cart',
onPressed: () {}
},
),
),
);
You can check if the snapshot has any data by using snapshot.data!.data()!.isNotEmpty
then
if(snapshot.data!.data()!.isNotEmpty){
//show your data widget
// using a variable might require you to call a setstate but since
//the widget is building here you might get some errors, its safe to just show your widget if data exists
}else{
///no data here
}
Also to get the .data() you need to tell your stream that its a of type <DocumentSnapshot<Map<String, dynamic>>> as . .snapshots() returns Stream<DocumentSnapshot<Map<String, dynamic>>>
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance..

I am trying to make a grocery app using flutter and firebase, everything is working but when I press the checkbox it Checks all of them

I made a floatingactionbutton and every time you press it it adds an item, and each item has a checkbox next to it but when I check off one item it checks all of them, I've spent a lot of time trying to figure out how to fix this but I can't. I could really use your help.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(FireApp());
}
class FireApp extends StatefulWidget {
#override
_FireAppState createState() => _FireAppState();
}
bool isChecked = false;
class _FireAppState extends State<FireApp> {
final TextController = TextEditingController();
#override
Widget build(BuildContext context) {
CollectionReference groceries =
FirebaseFirestore.instance.collection('groceries');
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: TextField(
controller: TextController,
),
),
body: Center(
child: StreamBuilder(
stream: groceries.orderBy('name').snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
return ListView(
children: snapshot.data!.docs.map((grocery) {
return Center(
child: Row(
children: [
Container(color: Colors.red,height: 50,child: Text(grocery['name'])),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: isChecked,
activeColor: Colors.black,
checkColor: Colors.greenAccent,
onChanged: (bool) {
setState(() {
isChecked = !isChecked;
});
}
)],
),
);
}).toList(),
);
},
),
),
floatingActionButton: FloatingActionButton(onPressed: () {
groceries.add({
'name': TextController.text,
});
},),
),
);
}
}
You are using the same variable for all your checkboxes (isChecked) but you ougth to have one per data, you could add that attribute to your firebase document so its synced or you could create it locally but each time your stream updates you will need to compare what grocery correspond to a checkbox value which can be hard.
UPDATE
The easiest way is to have a bool parameter in your Firestore document
Then just push an update any time the user tap
return ListView(
children: snapshot.data!.docs.map((grocery) {
return Center(
child: Row(
children: [
Container(color: Colors.red,height: 50,child: Text(grocery['name'])),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: grocery['checked'],
activeColor: Colors.black,
checkColor: Colors.greenAccent,
onChanged: (val) async {
final data = grocery.data();
data['checked'] = val;
await grocery.reference.update(data);
}
)],
),
);
}).toList(),
);
For now this is sufficient to answer your question, you will see later that this incurs in more Firestore calls, unnecesary rebuild of all widgets in the list and so on and you will have to think another way to optimize resources, like watching the stream somewhere else to have a local List of bools that keeps in sync all values of the groceries so you only update locally with an setState and once in the cloud at the end (a save button perhaps)

Flutter App Performance Issues with Long List

I have a flutter app where user can add items to a list which are stored in firebase. User can add up to 1000 items at once. Initially this is no issue but with a growing number of list items the app gets slower and slower until when adding multiple items at once after roughly 1000 items are in the list it crashes the app due to the memory use -
thread #10, name = 'io.flutter.1.ui', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=1450 MB, unused=0x0)
How can I improve the code so the performance improves. I would like to keep the setup with the Stream since it lets me dynamically filter the list on the fly. One information here as well is that WidgetA and WidgetB also both use the Stream Data to display the number of list items in the list.
Here is my code a bit simplified for ease of reading:
Main Screen Class:
Widget content(context) {
double h = MediaQuery.of(context).size.height; //screen height
double w = MediaQuery.of(context).size.width; //screen width
return StreamProvider<List<Activity>>.value(
catchError: (_, __) => null,
value: DatabaseService().activities(widget.uid),
builder: (context, snapshot) {
return SafeArea(
child: Container(
//color: Theme.of(context).backgroundColor, //SkyHookTheme.background,
child: Scaffold(
backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Stack(children: [
ListView(
controller: _scrollController,
children: <Widget>[
Column(
children: <Widget>[
WidgetA(),
WidgetB(),
ActivityList(), //List of User Activities
],
)
],
),
]),
),
),
),
);
});
}
ActivityList Class Listview Building:
ListView buildList(List<Activity> acts){
items = ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
itemCount: len,
itemBuilder: (context, index) {
return ActivityTile(activity: acts[index], number: acts.length - (index));
},
);
return items;
}
Any Tips / Hints how I can improve this would be highly appreciated.
Thanks!
You have to pagination to achieve smooth perform
And just load 10 documents in one time and with
Help of scrollcontroller check you are end of the list
And then load next 10 documents that’s would be
Efficient manner .
Instead of "listview" use sliversList widget.
See the Example of sliversList and sliverscomponents here
I think #AmitSingh's suggestion is best but if you want to load data in once then you can get data in pagination but not when the user scrolls but when you got the first bunch of data.
yeah you should use pagination or lazy-loading! reading and rendering 1000 document at once is too much work for most mobile devices.
instead you should load you documents likes this
import 'package:cloud_firestore/cloud_firestore.dart';
Firestore firestore = Firestore.instance
class LongList extends StatefulWidget {
#override
_LongListState createState() => _LongListState();
}
class _LongListState extends State<LongList> {
List<DocumentSnapshot> products = []; // stores fetched products
bool isLoading = false; // track if products fetching
bool hasMore = true; // flag for more products available or not
int documentLimit = 10; // documents to be fetched per request
DocumentSnapshot lastDocument; // flag for last document from where next 10 records to be fetched
ScrollController _scrollController = ScrollController(); // listener for listview scrolling
getProducts() async {
if (!hasMore) {
print('No More Products');
return;
}
if (isLoading) {
return;
}
setState(() {
isLoading = true;
});
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
print(1);
}
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];
products.addAll(querySnapshot.documents);
setState(() {
isLoading = false;
});
}
void initState(){
getProducts();
_scrollController.addListener(() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.20;
if (maxScroll - currentScroll <= delta) {
getProducts();
}
});
_pageManager = PageManager();
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Pagination with Firestore'),
),
body: Column(children: [
Expanded(
child: products.length == 0
? Center(
child: Text('No Data...'),
)
: ListView.builder(
controller: _scrollController,
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5),
title: Text(products[index]['name']),
subtitle: Text(products[index] ['short_desc']),
);
},
),
),
isLoading
? Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(5),
color: Colors.yellowAccent,
child: Text(
'Loading',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
)
: Container()
]),
);
}
}

StreamBuilder not updating after an item is removed Flutter

I am new to Flutter and this is my first time asking a question on Stackoverflow. I apologize for any misunderstanding. I will try my best to make it clear.
I am using sqflite for storing user's favorites and populating a list from the DB on a page, named Favorites screen. This Favorites page is one of the items on my bottom navbar.
My issue is that when I tap on an item from the favorites list which takes me to a screen where I can unfavorite that item. I double-checked that it is really removed from the DB by logging the rows count. But when I go back to the Favorites page, that item is still on the list. If I go to one of the pages from the bottom navbar and go back to the Favorites screen, the item isn't there. I understand that the page is being rebuilt again this time but my intention was the Stream will constantly listen for a change.
I have also implemented a slide to dismiss feature on the fav screen, which works as intended. But I am using the same logic on both.
StreamBuilder code in Favorite screen
StreamBuilder<List<WeekMezmurList>>(
stream: favBloc.favStream,
builder: (context, AsyncSnapshot<List<WeekMezmurList>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Text(
"Loading Favorites...",
style: TextStyle(fontSize: 20),
),
);
} else if (snapshot.data == null) {
return Center(
child: Text(
"No Favorites yet!",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
);
} else {
return ListView.builder(
physics: BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 10.0),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return new GestureDetector(
onTap: () =>
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
AudioPlayerScreen(
mezmurName: snapshot.data[index].mezmurName,
),
),
),
child: Slidable(
key: new Key(snapshot.data[index].mezmurName),
actionPane: SlidableDrawerActionPane(),
actionExtentRatio: 0.25,
// closes other active slidable if there is any
controller: slidableController,
secondaryActions: <Widget>[
IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () =>
_share(snapshot
.data[index]),
),
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () =>
_swipeDelete(
context, snapshot.data[index].mezmurName),
),
],
child: Card(
color: Colors.white,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 10.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
children: <Widget>[
_misbakChapter(
snapshot.data[index].misbakChapters),
SizedBox(width: 15),
_displayFavoritesMisbakLines(
snapshot.data[index], index),
],
)
],
),
),
),
),
);
},
);
}
},
);
slide to delete code in Favorites screen
// deletes the specific favorite from the sqflite db
Future<void> _swipeDelete(BuildContext context, String mezmurName) async {
try {
favBloc.delete(mezmurName);
} catch (e) {
CupertinoAlertDialog(
content: Text("Something went wrong. Please try again."),
actions: <Widget>[
CupertinoDialogAction(
child: Text(
"Ok",
),
onPressed: () => Navigator.of(context).pop(),
),
],
);
}
}
I have the same logic in the second screen, the screen I get when I tap on one of the items from the Fav list.
favBloc.delete(widget.mezmurName);
BLoC code, I got the concepts from this Medium article
class FavoritesBloc{
FavoritesBloc(){
getFavorites();
}
final databaseHelper = DatabaseHelper.instance;
// broadcast makes it to start listening to events
final _controller = StreamController<List<WeekMezmurList>>.broadcast();
get favStream => _controller.stream;
void dispose() {
_controller.close();
}
getFavorites () async{
_controller.sink.add(await databaseHelper.getFavorites());
}
insert(WeekMezmurList fav){
databaseHelper.insertToDb(fav);
getFavorites();
}
delete(String mezmurName){
databaseHelper.delete(mezmurName: mezmurName);
getFavorites();
}
}
Delete method in the DB class
// deleting a value from the db
delete({String mezmurName}) async {
var dbClient = await getDb;
try {
await dbClient
.delete(TABLE, where: '$MEZMUR_NAME = ?', whereArgs: [mezmurName]);
} catch (e) {
}
}
I have tried to research this issue but all I have found were for remote databases.
Just to make it more clear, I took a screen record.
Thank you in advance!
The reason why StreamBuilder on the first screen doesn't update with the changes made is because it uses a different instance of FavoritesBloc(). If you'd like for the bloc to be globally accessible with a single instance, you can declare it as
final favBloc = FavoritesBloc();
Otherwise, you can follow what has been suggested in the comments and pass FavoritesBloc as an argument between screens.

How to get use a list from future and use it inside a listView

I'm trying to get a list of all files in a certain directory.
I get the files from a future function called getUserVideos() if inside the function I try to printu the data, I can see the result, but I can't use the data outside the function.
class _mediaUtentiState extends State<mediaUtenti> {
var lightBlue = Color.fromRGBO(0, 197, 205, 1.0);
var _imagesDir;
#override
void initState() {
super.initState();
getUsersVideos();
}
List<String> Names = [
'Abhishek',
'John',
'Robert',
'Shyam',
'Sita',
'Gita',
'Nitish'
];
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: lightBlue,
appBar: new AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(padding: const EdgeInsets.all(8.0), child: Text('Nome')),
Container(
child: CircleAvatar(
backgroundImage: NetworkImage('http://i.pravatar.cc/300'),
),
),
],
),
backgroundColor: purple,
),
body: new Container(
child: new ListView.builder(
reverse: false,
itemBuilder: (_, int index) => EachList(this.Names[index]),
itemCount: this.Names.length,
),
),
);
}
Future<String> getUsersVideos() async {
print('something');
final Directory extDir = await getExternalStorageDirectory();
final String dirPath = '${extDir.path}/Movies/Veople';
final myDir = new Directory(dirPath);
List<FileSystemEntity> _images;
_images = myDir.listSync(recursive: true, followLinks: false);
print(_images.length);
_imagesDir = _images;
}
}
class EachList extends StatelessWidget {
final String name;
EachList(this.name);
#override
Widget build(BuildContext context) {
return new Card(
child: new Container(
padding: EdgeInsets.all(8.0),
child: new Row(
children: <Widget>[
new CircleAvatar(
child: new Text(name[0]),
),
new Padding(padding: EdgeInsets.only(right: 10.0)),
new Text(
name,
style: TextStyle(fontSize: 20.0),
)
],
),
),
);
}
}
for now I just show a list of names, but I want to show a card for each file in the path.
for example, in the function getUserVideos() whe I try to print imagesDir I get the right result [File: '/storage/emulated/0/Movies/Veople/1556217605345.mp4', File: '/storage/emulated/0/Movies/Veople/1556217605345.png', File: '/storage/emulated/0/Movies/Veople/1556217632709.mp4', File:
...]
But I cannot in any way access _imageDir out of that function.
I'm sure that is it possible to solve this problem with few lines, but right now it's 3 hours and I can't get a solution.
Thankyou!
I thought that for sure this would have already been answered, but while there's a lot of questions about FutureBuilder and Lists, none are quite like this or haven't really been answered adequately.
This is how I'd do it:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
Future<List<FileSystemEntity>> _getUsersVideos() async {
print('something');
final Directory extDir = await getExternalStorageDirectory();
final String dirPath = '${extDir.path}/Movies/Veople';
final myDir = new Directory(dirPath);
List<FileSystemEntity> _images = myDir.listSync(recursive: true, followLinks: false);
return _images;
}
class ListFromFuture extends StatefulWidget {
#override
_ListFromFutureState createState() => _ListFromFutureState();
}
class _ListFromFutureState extends State<ListFromFuture> {
Future<List<FileSystemEntity>> future;
#override
void initState() {
super.initState();
future = _getUsersVideos();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return Container(
alignment: Alignment.center,
child: Text("Loading"),
);
break;
case ConnectionState.done:
if (snapshot.hasError) {
// return whatever you'd do for this case, probably an error
return Container(
alignment: Alignment.center,
child: Text("Error: ${snapshot.error}"),
);
}
var data = snapshot.data;
return new ListView.builder(
reverse: false,
itemBuilder: (_, int index) => EachList(data[index]),
itemCount: data.length,
);
break;
}
},
);
}
}
The important parts of this are that:
future is only set it initState, not the build function. This makes sure that it isn't called each time the widget builds
I handle all of the cases where either there's an error or the future hasn't completed yet.
To be honest though, your example is actually very close to getting it working. All you'd have to do is wrap the line where you set _imagesDir = images in a setState(() => ...) and it should work (assuming the list doesn't return empty). You should also be checking for _imagesDir == null though, otherwise you might get null pointer exceptions.

Resources