I am working on Flutter application that uses Firestore. I have shown my documents in app with list view. I can update the documents and it appears immediately in app thanks to Flutter stream. However, I'm having an issue when I'm deleting a document. Seems deletion is working fine in reality, but the display in app is showing wrong information. For example, I delete a document at index 1, but always the document at last index is removed. Seems the event coming from FireStore stream just updating number of documents in stream, the list view is showing the information through another widget (like customer List tile) the information for each index is not updated correctly. Below is my code:
class _BudgetSummaryState extends State<BudgetSummary> {
Stream<QuerySnapshot> getSummaries() => FirebaseFirestore.instance
.collection('Users')
.doc(FirebaseAuth.instance.currentUser!.phoneNumber)
.collection('UserDBs')
.snapshots();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppBar('Summary'),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pushNamed('/Update_Edit_Screens/AddNewBudget');
},
label: const Text('Add new'),
icon: const Icon(Icons.add),
),
body: SafeArea(
child: StreamBuilder<QuerySnapshot>(
stream: getSummaries(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('Something went wrong, please try again later'),
);
}
if (snapshot.hasData) {
var data = snapshot.data;
if (data == null) {
return const Center(
child: Text('Add you first budget/expense tracker'),
);
} else {
return ListView.builder(
itemCount: data.docs.length,
itemBuilder: ((context, index) {
Color col =
index % 2 == 0 ? Colors.lightBlue : Colors.lime;
return SummaryWidget(data.docs[index], col);
}));
}
}
I did a workaround by reloading the whole page, that is rebuilding the complete route. But somehow I feel it kills the purpose of using stream. Please advise if I'm missing something?
Classic problem of data removal in Flutter.
You need to provide Key for each list item. You can use ValueKey with can take some unique id for each list item. Something like
return SummaryWidget(key:ValueKey(data.docs[index].id), data.docs[index], col);
Let me know if you want to know the reasoning, I will update here.
Related
I have an archaic solution for my problem, but it's quickly become cumbersome and not as robust. I want to switch to a riverpod provider and am studying how to do so, but I want to put this question out there at the same time. For simplicity, my app is similar to BlaBlaCar. Lots of trips listed, and then users can request to be picked up. The general flow goes like this:
To call the same document ID from Firestore, my solution has been just to pass a detailDocument parameter over and over. In my trips page, I have a streambuilder that displays the document:
StreamBuilder<QuerySnapshot<Object?>> tripStreamBuilder() {
return StreamBuilder<QuerySnapshot>(
stream: tripStream.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
return ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data =
document.data()! as Map<String, dynamic>;
return Card(
child: ListTile(
// leading: Text((data['departureDate']).toString()),
title: Text('From ${data['departure']}'),
subtitle: Text('To ${data['arrival']}'),
trailing: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text((data['departureDate']).toString()),
Text('Leaving at ${data['departureTime']}'),
],
),
),
onTap: () {
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
builder: (context) => TripDetails(
detailDocument: document,
)));
},
),
);
}).toList(),
);
},
);
From here, I just keep making detailDocument a parameter. For exmaple in my trip details:
class TripDetails extends ConsumerStatefulWidget {
const TripDetails({super.key, required this.detailDocument});
final DocumentSnapshot detailDocument;
And jointrip class:
class JoinTrip extends ConsumerStatefulWidget {
const JoinTrip({
super.key,
required this.detailDocument,
});
final DocumentSnapshot detailDocument;
So my question is how can I pass the document ID into a riverpod provider? And one that gets disposed and reset as users click on different trips.
You shouldn't pass the id of your document to pages. You can check this example from the official documentation.
In you case, you can have a provider that saves the id. So whenever you need to fetch or do something about the same id, you watch its value and when you need to change it, you can change the id in that provider.
Let's say we have two providers as follow:
// id provider
final tripIdProvider = StateProvider<String?>((ref) => null);
// Another provider fetching the document for an id
final tripProvider = Provider((ref) {
final id = ref.watch(tripIdProvider);
// code to call the Firebase and returning the result for instance.
});
tripIdProvider has the latest clicked id, and tripProvider will watch this id and updates and fetch the new values if the id changes. It fetches the document of that id from the Firebase.
Now if user clicks another object having another id, in the callback of pressing/tapping that object, you can do something like this:
...
onPressed:(){
ref.read(tripIdProvider.notifier).state = id;
}
This will change the id and because tripProvider is watching that providers' value, it will be triggered again and will fetch new data for the new id.
It's also good to check stateNotifier.
Ok so not sure if this is the best solution, but I ended up making a StateNotifierProvider out of the DocumentSnapshot class from Firestore.
class DocumentSnapshotNotifier extends StateNotifier<DocumentSnapshot?> {
DocumentSnapshotNotifier() : super(null);
DocumentSnapshot? get documentSnapshot => state;
set documentSnapshot(DocumentSnapshot? documentSnapshot) {
state = documentSnapshot;
}
void updateDocumentSnapshot(DocumentSnapshot? documentSnapshot) {
state = documentSnapshot;
}
}
final documentProvider = StateNotifierProvider<
DocumentSnapshotNotifier,
DocumentSnapshot?>((ref) => DocumentSnapshotNotifier());
Then once a user clicks on a trip, I pass the value to the provider via
onTap: () {
ref
.read(documentProvider.notifier)
.updateDocumentSnapshot(document);
Then I can just call it for whatever field in the doc I need.
Center(
child: Text(
'Departure Place: ${ref.read(documentProvider)!['departure']}')),
),
Center(
child:
Text('Arrival Place: ${ref.read(documentProvider)!['arrival']}')),
Center(
child: Text(
'Departure Date: ${ref.read(documentProvider)!['departureDate']}'))
It's working for now...
I'm still learning bloc patter. I created two pages, ViewPage and DetailsPage using a single bloc.
This is my bloc:
getRecordEvent
deleteRecordEvent
LoadedState
LoadedErrorState
DeletedState
DeletedErrorState
The view page will only build a widget with list of records on a LoadedState. When the user taps any record, It will push the Details page and displays detailed record with a delete button. When user press the delete button, I listen to the DeletedState and call the getRecord event to populate the view page again with the updated record.
Its all working but my problem is when I encountered an error while deleting record. When the state is DeleteErrorState, my view page becomes empty since I don't call getRecord there because the error could be internet connection and two error dialog will be shown. One for the DeletedErrorState and LoadedErrorState.
I know this is the default behavior of bloc. Do I have to create a separate bloc with only deleteRecordEvent? And also if I create a new page for adding record, will this also be a separate bloc?
UPDATE:
This is a sample of ViewPage. The DetailsPage will only call the deleteRecordEvent once the button was pressed.
ViewPage.dart
void getRecord() {
BlocProvider.of<RecordBloc>(context).add(
getRecordEvent());
}
#override
Widget build(BuildContext context) {
return
Scaffold(
body: buildBody(),
),
);
}
buildBody() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: BlocConsumer<RecordBloc, RecordState>(
listener: (context, state) {
if (state is LoadedErrorState) {
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return (WillPopScope(
onWillPop: () async => false,
child: ErrorDialog(
failure: state.failure,
)));
});
} else if (state is DeletedState) {
Navigator.pop(context);
getRecord();
} else if (state is DeletedErrorState) {
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return (WillPopScope(
onWillPop: () async => false,
child: ErrorDialog(
failure: state.failure,
)));
});
}
},
builder: (context, state) {
if (state is LoadedState) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
state.records.length <= 0
? noRecordWidget()
: Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: state.records.length,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: EdgeInsets.symmetric(
vertical: Sizes.s8),
child: ListTile(
title: Text(state.records[index].Name),
subtitle: state.records[index].date,
onTap: () {
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return BlocProvider<RecordBloc>.value(
value: BlocProvider.of<RecordBloc>(context),
child: WillPopScope(
onWillPop: () async => false,
child:
DetailsPage(record:state.records[index]),
));
});
},
),
));
}),
),
],
),
);
}
return (Container());
},
),
),
);
}
About bloc
As a general rule of thumb, you need one bloc per ui. Of course, this is not always the case, as it depends on a few factors, the most important of which is how many events are you handling in your ui. For your case, where there is a ui that holds a list of items into an item-details ui, I would create two blocs. One will only handle loading items (ItemsBloc for instance), the other will handle actions to a single item (SingleItemBloc). I might only use the delete event for now, but as the app grows, I will be adding more events. This all facilitates the Separation of Concerns concept.
Applying that to your case, the SingleItemBloc will handle deleting, modifying, subscribing, etc to a single item, while ItemsBloc will handle loading the items from the different repositories (local/remote).
Since I don't have the code for your bloc I can't offer any modifications.
Solution specific to your case
It seems that you're losing the last version of your list of items every time a new state is emitted. You should keep a local copy of the last list you acquired from your repositories. In case there is an error, you just use that list; if not just save the new list as the last list you had.
class MyBloc extends Bloc<Event, State> {
.....
List<Item> _lastAcquiredList = [];
Stream<State> mapEventToState(Event event) async* {
try {
....
if(event is GetItemsEvent) {
var newList = _getItemsFromRepository();
yield LoadedState(newList);
_lastAcquiredList = newList;
}
....
} catch(err) {
yield ErrorState(items: _lastAcquiredItems);
}
}
}
I am making a sms_app that can get data from the List of Objects when I press the button send the sms.Objects have a method which returns a stream. I am iterating through the data with ListViewbuilder and listening to stream via StreamViewBuilder. All the Object Streams are performing the function of sending the message. If i press the first send button ,then wait,press the second button and wait, it updates the UI accordingly.
But
UI is being updated for the last Stream. If I press the button one after another button without waiting. What am I doing wrong>.
Here is the Video
import 'package:flutter/material.dart';
import 'package:sms_maintained/sms.dart';
class SendSmsScreen extends StatelessWidget {
final SmsSender sender = SmsSender();
final SmsMessage message = SmsMessage('03009640742', '_body');
final List<SmsMessage> smsList = [
SmsMessage('03009750742', 'Random'),
SmsMessage('03008750742', 'Kaleem'),
SmsMessage('03056750742', 'Shahryar'),
SmsMessage('03127750742', 'Shahryar Zong')
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemCount: smsList.length,
itemBuilder: (context, index) {
return SingleChildScrollView(
child: Column(
children: [
IconButton(
icon: Icon(Icons.send),
onPressed: () async {
await sender.sendSms(smsList[index]);
},
),
StreamBuilder<SmsMessageState>(
key: GlobalKey(debugLabel: 'String $index'),
// initialData: SmsMessageState.None,
builder: (context, snapshot) {
if (snapshot.data == SmsMessageState.Sent) {
return Text('Message Sent');
}
if (snapshot.data == SmsMessageState.Delivered) {
return Text('Message Delivered');
}
if (snapshot.data == SmsMessageState.Sending) {
return Text('Sending');
}
if (snapshot.data == SmsMessageState.Fail)
return Text('Sending Failed');
return Text('Error');
},
stream: smsList[index].onStateChanged,
),
],
),
);
},
));
}
}
I'm assuming the UI doesn't update properly if pressing the button too quickly.
You can disable the button (example below) until the message is delivered you just need to set your button to be a child of the StreamBuilder.
onPressed: snapshot.data == SmsMessageState.Sending ? null : () async {
await sender.sendSms(smsList[index]);
}
That's because they are all executing the same method. When you do multiple executions in quick succession, last one stops the execution of previous. You'll have to find a way to queue them as they are executed and only do the next one when when the previous one is finished.
I was now trying for days to retrieve my firestore values, but no luck so posting it here.
I have a Firestore database and some data. I want to retrieve this with the help of Flutter.
This is what I have been doing.
So I have a Flutter screen where it shows a simple 3-dot dropdown in the AppBar.
It has two options: edit and cancel.
What I want is, when I press edit, it should open a new screen and should pass the data that I retrieved from firestore.
This is where I have edit and cancel dropdown (3 dots) and calling the a function (to retrieve data and open the new screen).
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(widget.news.headline.toUpperCase()),
actions: <Widget>[
PopupMenuButton<String>(
onSelected: (value) {
_open_edit_or_delete(value); // caling the function here
},
itemBuilder: (BuildContext context) {
return {'Edit', 'Delete'}.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
),
],
),
body: _get_particular_news(widget.news),
);
}
and this is the open_edit_or_delete function it is calling. But it doesn't open up (navigate) to the screen I am calling.
open_edit_or_delete(String selectedOption) {
News news;
Visibility(
visible: false,
child: StreamBuilder(
stream: FireStoreServiceApi().getNews(),
builder: (BuildContext context, AsyncSnapshot<List<News>> snapshot) {
if (snapshot.hasError || !snapshot.hasData) {
Navigator.push(
context, MaterialPageRoute(builder: (_) => FirstScreen(news:news)));
return null;
} else {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
news = snapshot.data[index];
},
);
}
},
));
}
And in case you need the FireStoreServiceApi().getNews(), here it is as well.
// get the news
Stream<List<News>> getNews() {
return _db.collection("news").snapshots().map(
(snapshot) => snapshot.documents
.map((doc) => News.fromMap(doc.data, doc.documentID))
.toList(),
) ;
}
Can someone please help me?
You are not passing data correctly to your fromMap method.
You can access data using doc.data['']
If you have data and documentID property in it then following will work.
News.fromMap(doc.data.data, doc.data.documentID))
I don't know your fromMap method and i also don't what your snapshot contains, if this did not work for you then add them too.
I am fetching articles from HackerNews API using Bloc Pattern and Streams.
I am loading all the articles and presenting in the UI with the help of a stream builder, and this works fine.
Now I wrapped the article fetching Stream builder with the new loading StreamBuilder.
Now when the loading stream builder has true (means it is loading) it shows a circular progress indicator or else, it shows the child (Article List wrapped with a Stream Builder).
This works fine. But it is bugging me that I have wrapped Stream builder inside a stream builder. I know I can take help of rxdart but I am just not sure how.
I tried to add a loader with the help of snapshot.hasData or not but that didn't work, so I decided to create another stream and subject that takes a bool and tells the UI if it is loading or not.
Code fetching data int the bloc:
_getAndUpdateArticles(StoryType storyType) {
_isLoadingSubject.add(true);
getListIds(storyType).then((list) {
getArticles(list.sublist(0, 10)).then((_){
_articleSubject.add(UnmodifiableListView(_articles));
_isLoadingSubject.add(false);
});
});
}
UI:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: StreamBuilder(
stream: widget.hnBloc.isLoading,
builder: (context, snapshot) {
if (snapshot.data) {
return Center(child: CircularProgressIndicator());
} else {
return StreamBuilder<UnmodifiableListView<Article>> (
initialData: UnmodifiableListView<Article>([]),
stream: widget.hnBloc.article,
builder: (context, snapshot) => ListView(
children: snapshot.data.map(_buildItem).toList(),
),
);
}
},
),
.........
EDIT
I have tried this, but this isn't working:
StreamBuilder<UnmodifiableListView<Article>> (
initialData: UnmodifiableListView<Article>([]),
stream: widget.hnBloc.article,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView(
children: snapshot.data.map(_buildItem).toList(),
);
} else {
return CircularProgressIndicator();
}
}
),
I Don't think there is a complete way to avoid nested StreamBuilders. I personally wouldn't consider it a bad practice, but it will definitely lead to more build.
In your case, You can modify your hnBloc to emit a single state that can be a loading state or data state , thereby eliminating the need for a nested StreamBuider.
eg.
StreamBuilder<HnState>(
stream: hnBloc.currentState,
initialData: HnLoadingState(),
builder: (context, snapshot) {
if (snapshot.data is HnLoadingState) {
return Center(child: CircularProgressIndicator());
}if (snapshot.data is HnDataState) {
return ListView(
children: snapshot.data.map(_buildItem).toList(),
),
}
},
)
This pattern is very common when using the flutter_bloc package. You can see a basic example of this here to understand it better.