StreamBuilder not updating after an item is removed Flutter - sqlite

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.

Related

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 - list duplicates old and new values

I have a method for receiving push from Firebase Cloud Messaging, and within it, the notification is allocated within a list. What happens is that this method is making the old value a duplicate of the new value.
Firebase method:
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
if (message.notification != null) {
widget._localNotificationList.title = message.notification.title;
widget._localNotificationList.body = message.notification.body;
widget._pushDecode.notifList.add(widget._localNotificationList);
savePush(); //this method maintains notification on the user's screen, with sharedPreferences
setState(() {});
}
});
Page View:
ListView.builder(
itemCount: widget._pushDecode.notifList.length,
itemBuilder: (context, i) {
return Card(
margin: EdgeInsets.all(10),
elevation: 4,
child: ListTile(
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => removePush(i),
),
title: Text(
widget._pushDecode.notifList.reversed.elementAt(i).title,
),
subtitle: Text(
widget._pushDecode.notifList.reversed.elementAt(i).body,
),
),
);
},
),
You need to use the key value because it does not know if the widget that was in the position you are adding it for example is different then the previous, but using keys he can always know that two widgets are different when they are the same type like your card.
ListView.builder(
itemCount: widget._pushDecode.notifList.length,
itemBuilder: (context, i) {
return Card(
key: UniqueKey(), // you can also use ValueKey('this must be unique for each element in the list.')
margin: EdgeInsets.all(10),
elevation: 4,
child: ListTile(
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => removePush(i),
),
title: Text(
widget._pushDecode.notifList.reversed.elementAt(i).title,
),
subtitle: Text(
widget._pushDecode.notifList.reversed.elementAt(i).body,
),
),
);
},
),

Each page showing Favorite button and button to be saved locally to reload whenever the app restarted to see the last status page by page

I need help for favorite button; is anyone help me for that?
I have pageViews and favorite buttons. Buttons are working but when I favorited one page all others are also favorited.
I recorded all favorite pages to another page and showing as listView. I can delete them as well from listView item by item.
I am fetching json data from API and recording locally to work on. In addition to that, I need to save each page's favorite status locally to reload them whenever the app restarted.
I checked lost of examples, but found too complicated since I am new. I need brief and simple example to study on it.
If you help me for that it would be appreciated.
'''
body: FutureBuilder(
future: ApiProvider().getData(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: const CircularProgressIndicator());
} else {
_itemLength = snapshot.data.length;
_isFavorited = List<int>.generate(_itemLength, (_) => 0);
return PageView.builder(
itemCount: _itemLength,
itemBuilder: (context, index) {
_currentIndex = index;
/// ========= JSON DATA ==========================
final data = snapshot.data[index];
jsonId = data['id'];
quoteId = data[quoteId];
passTitle = data['title'].toString();
return Container(
margin: EdgeInsets.all(20),
child: ListView(
scrollDirection: Axis.vertical,
shrinkWrap: false,
physics: BouncingScrollPhysics(),
children: <Widget>[
CachedNetworkImage(
width: 80,
height: 80,
fit: BoxFit.scaleDown,
imageUrl: data['image_url'].toString(),
placeholder: (context, url) => Center(child:
CircularProgressIndicator()),
errorWidget: (context, url, error) => Icon(Icons.error),
),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(bottom: 0),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
key: Key('icon_$index'),
padding: EdgeInsets.all(0),
alignment: Alignment.centerRight,
icon: (_isFav ? Icon(Icons.favorite, size:
35) : Icon(Icons.favorite_border, size: 35)),
color: Colors.red[500],
onPressed: () {
print('_isFavorited onPressed :
$_isFavorited');
{
setState(() {
if (_isFav == true) {
_isFav = false;
print('Favorite changed to: Off');
isFavorite = _isFavorited[index] = 0;
setState(() {
dbHelper.deleteQuote(jsonId);
_showSnackBar(context, "Favorite
Quote deleted");
});
} else {
_isFav = true;
debugPrint('Favorite On');
isFavorite = _isFavorited[index] = 1;
var quote = Quote(quoteId, passTitle,
jsonId, isFavorite);
dbHelper.saveQuote(quote);
}
});
}
}
),
),
],
),
),
],
),
SizedBox(height: 10),
Container(child: Text(data['title'], style:
GoogleFonts.satisfy(fontSize: 25, fontWeight:
FontWeight.bold), key: Key('text_$index'))),
],
),
);
},
);
}
},
),
'''

Flutter asyncMap not run until setState

I am making a chat app that displays both a Group Chat and Private Chat in the same List.
I use Firestore as the database and store the data of User, Group and Contact in there. I have a Message Screen that displays a list of Chats that the User has using StreamBuilder.
I want to display data differently depending on the group's data. The group chat has their Group picture, Private Chat with User in Contact, their avatar display, and Private Chat with a generic icon display with User not in Contact.
I iterate through the stream first in a DatabaseService class, then put it in a variable and set it as a stream for StreamBuilder. This works fine, but I also want a list to check if a user already has a private chat with another User without getting the data from Firestore.
API.dart
//this is where I put my code to connect and read/write data from Firestore
final FirebaseFirestore _db = FirebaseFirestore.instance;
Api();
....
Stream<QuerySnapshot> streamCollectionByArrayAny(
String path, String field, dynamic condition) {
return _db
.collection(path)
.where(field, arrayContainsAny: condition)
.snapshots();
}
DatabaseService.dart
...
List<GroupModel> groups; //List of Groups
Stream<List<GroupModel>> groupStream; //Stream of List Group
...
Stream<QuerySnapshot> fetchGroupsByMemberArrayAsStream(
String field, dynamic condition) {
return _api.streamCollectionByArrayAny('groups', field, condition);
}
//function to get Contact Detail using List of Group User
Future<ContactModel> getContactDetail(List<dynamic> members) async {
//remove current user id from the list
members.removeWhere((element) => element.userId == user.userId);
//getContactbyId return a ContactModel object from Firestore
ContactModel contactModel =
await getContactById(user.userId, members.first.userId);
if (contactModel != null && contactModel.userId.isNotEmpty) {
return contactModel;
} else {
return new ContactModel(
userId: members.first.userId, nickname: "", photoUrl: "");
}
}
Future<GroupModel> generateGroupMessage(GroupModel group) async {
//check if Group Chat or Private chat
if (group.type == 1) {
ContactModel contactModel = await getContactDetail(group.membersList);
group.groupName = contactModel.nickname.isNotEmpty
? contactModel.nickname
: contactModel.userId;
group.groupPhoto = contactModel.photoUrl;
}
print("Add");
//add the group data into List<GroupModel> groups
groups.add(group);
return group;
}
void refreshMessageList() {
groups = [];
print("refresh");
//get Group Data as Stream from FireStore base on the user data in the Member Array of Group then map it to Stream while also change data base on Group type in generateGroupMessage
groupStream = fetchGroupsByMemberArrayAsStream('membersList', [
{"isActive": true, "role": 1, "userId": user.userId},
{"isActive": true, "role": 2, "userId": user.userId}
]).asyncMap((docs) => Future.wait([
for (GroupModel group in docs.docs
.map((doc) => GroupModel.fromMap(doc.data()))
.toList())
generateGroupMessage(group)
]));
}
Message.dart
#override
void initState() {
super.initState();
...
databaseService.refreshMessageList();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.symmetric(horizontal: 16),
margin: EdgeInsets.only(top: 24),
child: Column(
children: [
...
Flexible(
child: StreamBuilder(
stream: databaseService.groupStream,
builder: (context, AsyncSnapshot<List<GroupModel>> snapshot) {
if (!snapshot.hasData) {
print("No data");
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
);
} else {
print("Has data");
groups = List.from(snapshot.data);
groups.removeWhere(
(element) => element.recentMessageContent.isEmpty);
groups.sort((group1, group2) {
if (DateTime.parse(group1.recentMessageTime)
.isAfter(DateTime.parse(group2.recentMessageTime))) {
return -1;
} else {
return 1;
}
});
return ListView.builder(
padding: EdgeInsets.all(10.0),
itemBuilder: (context, index) =>
buildItem(context, groups[index]),
itemCount: groups.length,
),
),
),
}
],)));
}
Widget buildItem(BuildContext context, GroupModel group) {
if (group.recentMessageContent == '') {
return Container();
} else {
return Column(
children: [
Container(
child: InkWell(
child: Row(
children: <Widget>[
Material(
child: group.groupPhoto.isNotEmpty
? CachedNetworkImage(
placeholder: (context, url) => Container(
child: CircularProgressIndicator(
strokeWidth: 1.0,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.grey),
),
width: 60.0,
height: 60.0,
padding: EdgeInsets.all(10.0),
),
imageUrl: group.groupPhoto,
width: 60.0,
height: 60.0,
fit: BoxFit.cover,
)
: Icon(
group.type == 1
? Icons.account_circle
: Icons.group,
size: 60.0,
color: Colors.grey,
),
borderRadius: BorderRadius.all(Radius.circular(30.0)),
clipBehavior: Clip.hardEdge,
),
SizedBox(
width: 150,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
group.groupName,
style: TextStyle(
color: colorBlack,
fontSize: 12,
fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Text(
group.recentMessageContent,
style: TextStyle(
color: Colors.grey,
fontSize: 10,
height: 1.6),
overflow: TextOverflow.ellipsis,
),
],
),
margin: EdgeInsets.only(left: 12.0),
),
),
Spacer(),
Text(
formatDateTime(group.recentMessageTime),
style: TextStyle(color: Colors.grey, fontSize: 10),
),
],
),
onTap: () {
switch (group.type) {
case 1:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
settings:
RouteSettings(name: "/message/chatPage"),
builder: (context) => ChatPage(group: group)))
.then((value) => setState);
break;
case 2:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
settings:
RouteSettings(name: "/message/chatGroup"),
builder: (context) =>
ChatGroupPage(group: group)))
.then((value) => {setState(() {})});
break;
}
}),
),
Divider(
color: Colors.grey,
),
],
);
}
}
The ChatPage and ChatGroupPage navigate to Private Chat and Group Chat respectively, and in there the User can add the chat partner or group member into Contact.
When adding is done I call the databaseService.refreshMessageList to refresh the Stream of List Group, so when I navigate back to the Message Screen, it will refresh and display accordingly. However, the List<GroupModel> groups becomes blank and will not add data until I navigate back to the Message Screen.
I debugged the app and found that the List became blank because it executes groups = [] but did not run the .asyncMap until I hot reload or navigate Message Screen and put the setState in .then to refresh the data.
I need the List groups to check whether the 2 users already have a private chat to create a new one when adding to Contact. I have already tried putting setState after databaseService.refreshMessageList, but it still did not work.
Can anyone please help me and provide a solution? I know this is not a good question to ask, but I have been stuck with this for almost a week now and desperately need an answer. Thank you in advance.
EDIT
Here is my data structure:
Users
/users (collection)
/userId
/user (document)
- userId
- nickname
- photoUrl
- token
- /contacts (subcollection)
/contactId
/contact (document)
- userId
- nickname
- photoUrl
Groups:
/groups (collection)
/groupId
/group (document)
- groupId
- groupName
- type
- membersList (List<Map<String, dynamic>>)
- member: userId, isActive, role
- recentMessageContent
- recentMessageTime
- recentMessageType
Messages:
/messages (collection)
/groupId
/groupMessage (document)
/messages (subcollection)
/messageId
/message (document)
- messageContent
- messageTime
- messageType
You can use array membership, for example, the array-contains method can query for elements within an array without performing any manipulation. There is an interesting article that provides some examples you might interest you.
Another alternative could be to iterate both arrays until matching the values you need. However, iteration can lead to performance issues if you do not implement it correctly.

Cant get StreamBuilder to display data from cloud firestore

I know I have a connection to the database and no errors are appearing so I'm pretty confused. The title and code should summarize the problem fairly well. Think I'm missing something?
here is the main code that should be displaying cards with titles from firebase
mainList() {
StreamBuilder(
stream: Firestore.instance.collection('Events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('Loading');
} else {
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot userPost = snapshot.data.documents[index];
return Container(
width: MediaQuery.of(context).size.width,
height: 350.0,
child: Padding(
padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Material(
elevation: 14.0,
shadowColor: Color(0x802196F3),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width,
height: 200.0,
child: Text(
'${userPost['title']}',
))
],
),
),
))),
);
},
);
}
});
}
and here is where the function is called:
lass MyAppmain extends State<MyApp> {
#override
Widget build(BuildContext context) {
var listView = ListView.builder(
itemCount: local.length,
itemBuilder: (BuildContext cnxt, int index) {
return new Text(local[index]);
});
return MaterialApp(
home: PageView(
controller: controller,
children: <Widget>[
//home page---------------------------
Scaffold(
appBar: AppBar(
title: Text(
'Events',
),
elevation: 20,
),
//main list view for the cards
//think I use streambuilder for this so google before starting
body: mainList(),//RIGHT HERE
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.push(context, NewEventTransition());
},
mini: true,
),
),
//Profile Page-------------------------------
Scaffold(
appBar: AppBar(
title: Text(
'Profile',
),
elevation: 20,
),
),
],
));
}
}
Want a listview of cards holding the titles from firebase (will soon be more than titles but want to get this working first)
This is a common problem.
return ListView.builder(
itemCount: snapshot.data.documents.length, // this line is the culprit!
itemBuilder: (context, index) {
print(snapshot.data.documents.length); // it will print null
.......
}
See, It takes some time to fetch data from firebase. When ListView.builder is called the value of snapshot.data.documents.length is actually null. Tho after few seconds it gets data but till then ListView had built the UI and that's why it's blank. To check the value, you can add a Print statement like shown above.
Now there are few ways to solve this problem:
Make an int variable say totalLength, make a function say setTotalLength which makes a call to Firebase/Firestore database and use setState to assign this value to totalLength and then change that code to this:
itemCount: totalLength,
You should Call setTotalLength in your initState method.
Or, you can change your code to this, But I'm NOT 100% sure that this will work:
itemCount: snapshot.data.documents.length ?? 0 // returns 0 if the value is null, may reload UI again when data comes

Resources