How Should I Deal with Null Values in a StreamBuilder? - flutter

I'm trying to build a flutter view that loads a list of items ('cost codes' in the code snippet) from a database call. This code works elsewhere in my project where I already have data in the database, but it fails when it tries to read data from an empty node. I can provide dummy data or sample data for my users on first run, but they might delete the data before adding their own, which would cause the app to crash the next time this view loads.
What's the proper way to deal with a potentially empty list in a StreamBuilder?
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: dbPathRef.onValue,
builder: (context, snapshot) {
final costCodes = <CostCode>[];
if (!snapshot.hasData) {
return Center(
child: Column(
children: const [
Text(
'No Data',
style: TextStyle(
color: Colors.white,
),
)
],
),
);
} else {
final costCodeData =
// code fails on the following line with the error
// 'type "Null" is not a subtype of type "Map<Object?, dynamic>" in type cast'
(snapshot.data!).snapshot.value as Map<Object?, dynamic>;
costCodeData.forEach(
(key, value) {
final dataLast = Map<String, dynamic>.from(value);
final account = CostCode(
id: dataLast['id'],
name: dataLast['name'],
);
costCodes.add(account);
},
);
return ListView.builder(
shrinkWrap: false,
itemCount: costCodes.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
costCodes[index].name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
costCodes[index].id,
style: const TextStyle(color: Colors.white),
),
);
},
);
}
},
);
}

Personally I tend to avoid handling raw data from a database in the UI code and handle all of this in a repository/bloc layer.
However, to solve your issue you can simply add a ? to the end of the cast like so:
final costCodeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>?;
You will no longer get the cast exception - however you still have to test costCodeData for null.
This block of code may help:
final data = snapshot.data;
final Map<Object?, dynamic>? costCodeData
if (data == null) {
costCodeData = null;
} else {
costCodeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>?;
}
if (costCodeData == null){
// Show noData
} else {
// Show data
}

final dataLast = Map<String, dynamic>.from(value);
final account = CostCode(
id: dataLast['id'],
name: dataLast['name'],
);
costCodes.add(account);
},
you declaired dataLast with a Map having key as String, but inside the account variable the id and name are not in the string format, keep those inside "" || '' even after modiying these, if you still face other issue try putting question mark at the end of the line
(snapshot.data!).snapshot.value as Map<Object, dynamic>?

Related

Why Won't This StreamBuilder Display Data without Restarting the App?

I'm trying to get a Flutter project to display a simple list of items returned from the Firebase Realtime Database. The code mostly works, but I have to restart the app each time I log out and log back in as a different user, which isn't what I'm looking for. I need the user's data to appear when they log in. I don't quite understand what all is happening here (I stumbled across a functional solution after several days of trial and error and googling), but I thought a Stream was more or less a 'live' stream of data from a particular source.
EDIT: kPAYEES_NODE is a constant stored elsewhere that resolves to 'users/uid/payees' in the RTDB:
import 'package:firebase_database/firebase_database.dart';
import 'auth_service.dart';
final DatabaseReference kUSER_NODE =
FirebaseDatabase.instance.ref('users/${AuthService.getUid()}');
final DatabaseReference kPAYEES_NODE = kUSER_NODE.child('payees');
Here's the code in question:
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...),
body: StreamBuilder(
stream: kPAYEES_NODE.onValue,
builder: (context, snapshot) {
final payees = <Payee>[];
if (!snapshot.hasData) {
return Center(child: Column(children: const [Text('No Data')]));
} else {
final payeeData =
(snapshot.data!).snapshot.value as Map<Object?, dynamic>;
payeeData.forEach((key, value) {
final dataLast = Map<String, dynamic>.from(value);
final payee = Payee(
id: dataLast['id'],
name: dataLast['name'],
note: dataLast['note'],
);
payees.add(payee);
});
return ListView.builder(
shrinkWrap: true,
itemCount: payees.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
payees[index].name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
payees[index].id,
style: const TextStyle(color: Colors.white),
),
);
});
}
},
),
floatingActionButton: (...),
);
}
}
That's because you only get the UID of the current once in your code, here:
FirebaseDatabase.instance.ref('users/${AuthService.getUid()}')
And by the time this line runs, the AuthService.getUid() has a single value. Instead, the code needs to be reevaluated any time a users signs in or out of the app.
In an app where the user can sign in and out, the UID values are a stream like the one exposed by FirebaseAuth.instance.authStateChanges() as shown in the Firebase documentation on getting the current user. You can wrap the stream that is returned by authStateChanges() in a StreamBuilder, and then create the database reference inside of that builder, to have it respond to changes in the authentication state.

Flutter FutureBuilder does not stop showing CircularProgressIndicator

I am trying to receive data using a FutureBuilder, but it hangs on the CircularProgressIndicator. I think it's remaining on ConnectionState.waiting but I'm not sure why.
#override
initState() {
_widgetList = getWidgetList();
}
Stream<List<String>> getFriendUUIDS() => Firestore.friends
.doc(gameManager.myself.uuid)
.snapshots()
.map((snapshot) => ((snapshot.data()?.keys)?.toList()) ?? []);
Future<List<MomentWidget>> getWidgetList() async{
List<MomentWidget> widgetList = [];
Set<String> momentIds = Set();
await for (final uuids in getFriendUUIDS()){
for (String uuid in uuids){
DocumentSnapshot<Map<String, dynamic>> values = await Firestore.users
.doc(uuid)
.get();
for (String momentId in values.data()?['moments'] ?? [] ){
momentIds.add(momentId);
}
}
}
for (String momentId in momentIds){
DocumentSnapshot<Map<String, dynamic>> values =
await Firestore.instance.collection('moments').doc(momentId).get();
Map<String, dynamic>? data = values.data()!;
String downloadURL = await storage.ref('moments/$momentId').getDownloadURL();
MomentWidget widget = MomentWidget(numLikes: data['liked_by'].length ,
location: data['location'],
date: data['timestamp'],
names: data['taggedFriends'].toString(),
shotBy: data['taken_by'], image: NetworkImage(downloadURL));
widgetList.add(widget);
}
return widgetList;
}
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
height: size.height,
width: size.width,
child: FutureBuilder(
future: _widgetList,
builder: (context, AsyncSnapshot<List<MomentWidget>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, pos) {
return snapshot.data![pos];
},
);
}
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
default:
return Text('Unhandled State');
}
}
),
);
}
I have tried to get the Future inside of initState(), and have tried to use snapshot.hasData instead, to no avail.
I have encountered a similar problem. When building an object from json , if the types don't match , it can quietly fail. I do not think your widgetList is ever returned. In my case I had a variable "cost" that I thought would be of type int , however in the database it was of type String. It always quietly failed and never showed the markers on the map widget
So:
Check how many times that loop of yours is executed. Probably only once and then it quietly fails
If the above happens:
Makes sure the types of your variables match the ones from the database. Comment out every variable one by one to find where the problem is.
Let me know if it works

(Flutter - Firestore) The getter 'documents' was called on null

I have stored the download links of images on Firestore while the images are in firebase storage.
I am trying to retrieve the links and display them via stream builder but I'm encountering an error.
What can I do to fix this.
StreamBuilder:
StreamBuilder(
stream: db
.collection("Highway Secondary School Announcements")
.doc()
.snapshots(),
builder: (context, snapshot) {
if (snapshot!= null && snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return Center(
child: Text("Snapshot Was Not Retrieved"),
);
}
for (int i = 0; i < snapshot.data.documents; i++) {
listOfUrls.add(snapshot.data.documents[i]['url']);
listOfPics.add(Padding(
padding: const EdgeInsets.only(top: 50, bottom: 50),
child: Image.network(listOfUrls[i]),
));
}
return Container(
child: ListView(
children: listOfPics,
),
);
}),
Error:
The getter 'documents' was called on null.
Receiver: null
Tried calling: documents
If you have doc(), you need to specify which documentation(doc('name')) you want to be read, otherwise, you could remove ".doc()".
Reference:Cloud Firestore
before
db.collection("Highway Secondary School Announcements").doc().snapshots()
after
db.collection("Highway Secondary School Announcements").snapshots()
Second question:
I used "final items = snapshot.data?.docs" to get documents from that snapshot.
Here has a nice example Cloud Firestore flutter
final items = snapshot.data?.docs.reversed;
for ( var item in items!) {
final itemName = item.get('name');
final itemLogo = item.get('logo');
final itemDate = item.get('date');
// String itemDate2 = DateTime.fromMillisecondsSinceEpoch(itemDate).toString();
final itemBubble = _getListItemWidget(
iconName: itemLogo,
titleName: itemName,
subTitleName: DateTime.now(),
scoreKeeper1: scoreKeeper1,
scoreKeeper2: scoreKeeper2,
scoreKeeper3: scoreKeeper3
);
itemBubbles.add(itemBubble);
}

how to let consumer listen to multiple parameters in flutter?

I need to let the consumer widget listen to multiple variables depending on a boolean value.
this is the model class
class Lawyer{
Data? data;
double? distance = 0;
Lawyer({this.data, this.distance});
factory Lawyer.fromJson(Map<String, dynamic> json) =>
Lawyer(data: Data.fromJson(json['listing_data']));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
class Data{
String? title;
String? email;
String? phone;
Location? location;
List<String>? logo;
List<String>? cover;
Data({this.title, this.email, this.phone, this.logo, this.cover, this.location});
factory Data.fromJson(Map<String, dynamic> json) {
var logo = json['_job_logo'];
var cover = json['_job_cover'];
var long = json['geolocation_long'];
var lat = json['geolocation_lat'];
return Data(title: json['_job_tagline'], email: json['_job_email'],
location: Location(latitude: json['geolocation_lat'], longitude: json['geolocation_long']),
phone: json['_job_phone'], logo: List<String>.from(logo),
cover: List<String>.from(cover)
);
}
}
and this is the view model notifier
class LawyerAPIServices extends ChangeNotifier{
final url = "https://dalilvision.com/wp-json/wp/v2/job_listing";
List<Lawyer> lawyersList = [];
List<Lawyer> staticLawyersList = [];
Future<List<Lawyer>> fetchLawyers() async{
final response = await get(Uri.parse(url.toString()));
if(response.statusCode == 200){
var dynamicLawyersList = jsonDecode(response.body);
print('$dynamicLawyersList');
lawyersList = List<Lawyer>.from(dynamicLawyersList.map((x) => Lawyer.fromJson(x)));
staticLawyersList = lawyersList;
lawyersList.forEach((element) {print('all lawyers: ${element.data!.location}');});
notifyListeners();
return lawyersList;
}
else{
notifyListeners();
throw Exception(response.statusCode);
}
}
Future<List<Lawyer>> getFullListOfLawyers() async {
notifyListeners();
print('fulll list: ${staticLawyersList.length}');
return staticLawyersList;
}
}
and finally this is the consumer widget
Consumer<LawyerAPIServices>(
builder: (context, value, child) => FutureBuilder(
future: _list,
builder: (BuildContext context, AsyncSnapshot<List<Lawyer>> snapshot) {
if (snapshot.hasData){
return ListView.separated(
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
shrinkWrap: true,
separatorBuilder: (context, index) => const Divider(color: Colors.transparent),
itemCount: value.lawyersList.length,
itemBuilder: (context, index) {
return InkWell(
child: LawyerWidget(
title: snapshot.data![index].data!.title!,
email: snapshot.data![index].data!.email!,
phone: snapshot.data![index].data!.phone!,
logo: snapshot.data![index].data!.logo![0],
cover: snapshot.data![index].data!.cover![0]
),
);
}
}
);
}
else if(snapshot.hasError){
return Center(
child: Text(snapshot.error.toString())
);
}
else {
return const CircularProgressIndicator(
strokeWidth: 2,
);
}
},
),
)
In the notifier class there are two lists, the staticLawyerList is initialized only once when getting the list from a network call and then used as a backup list, and the lawyersList is the one that will be manipulated.
what I have done until now is to get the initial value of lawyersList by a network call, then somehow the staticLawyersList values are always equal to lawyersList, even if I made any change or manipulate the lawyersList these changes will automatically reflect on the staticLawyersList which is really weird.
now what I want to achieve exactly is to apply a condition to update the UI with the appropriate list depending on this condition.
if(setByPosition == false){
//update UI with `staticLawyersList`
}
else {
//update UI with `lawyersList`
}
update!!!!!!!!
here's how I update my consumer
CheckboxListTile(
activeColor: Colors.black,
value: isChecked,
onChanged: (value) async {
saveSharedPreferences(value: value!);
if(value == true) {
Provider.of<LawyerAPIServices>(context, listen: false).sortLawyersList(
devicePosition: widget.position, lawyersList: widget.list);
}
else{
Provider.of<LawyerAPIServices>(context, listen: false).getFullListOfLawyers();// the list returned by this function don't applied to the consumer
}
setState(() {
isChecked = value;
Navigator.pop(context);
});
},
title: const Text('Filter by distance'),
),
A few things to consider:
When you do this "staticLawyersList = lawyersList" you actually have two "pointers" to the same list. It works that way for lists, sets, classes, etc.. only basic types as int, double, string are really copied.
You can use this instead: "staticLawyersList = List.from(lawyersList);"
It doesn't seem you need the ChangeNotifier in your LawyerAPIServices. You could create an instance of LawyerAPIServices in the widget you need it and call fetchLawyers. Do it in the initState of a StatefullWidget if you don't want the list to be rebuilt multiple times. In your build method use a FutureBuilder to read the Future and decide what to show in the UI.
class _MyWidget extends State<MyWidget> {
late final LawyerAPIServices lawyerApi;
// Create this variable to avoid calling fetchLawers many times
late final Future<List<Lawyer>> lawyersList;
#override
void initState() {
super.initState();
// Instantiate your API
lawyerApi = LawyerAPIServices();
// This will be called only once, when this Widget is created
lawyersList = lawyerApi.fetchLawyers();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Lawyer>>(
future: lawyersList,
builder: ((context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
if (setByPosition) {
//update UI with `lawyersList`
return _listView(snapshot.data!);
} else {
//update UI with `staticLawyersList`
// Since the Future state is Complete you can be sure that
// the staticLawyersList variable in your API was already set
return _listView(lawyerApi.staticLawyersList);
}
case ConnectionState.none:
return const Text('Error');
default:
return const CircularProgressIndicator.adaptive();
}
}),
);
}
Widget _listView(List<Lawyer> lawyersList) {
return ListView.separated(
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
shrinkWrap: true,
separatorBuilder: (context, index) =>
const Divider(color: Colors.transparent),
itemCount: lawyersList.length,
itemBuilder: (context, index) {
return InkWell(
child: LawyerWidget(
title: lawyersList[index].data!.title!,
email: lawyersList[index].data!.email!,
phone: lawyersList[index].data!.phone!,
logo: lawyersList[index].data!.logo![0],
cover: lawyersList[index].data!.cover![0]),
);
});
}
}
If for any reason you need to share the same LawyerAPIServices across multiple widgets, you could instantiate it on the top of your tree and send it down using Provider or as a parameter.
The method getFullListOfLawyers doesn't need to return a Future, since staticLawyersList is a List (not a Future). You could get this list directly using "LawyerAPIServices.staticLawyersList" or maybe something like this could make sense:
Future<List> getFullListOfLawyers() async {
if(staticLawyersList.isEmpty) {
await fetchLawyers();
}
print('fulll list: ${staticLawyersList.length}');
return Future.value(staticLawyersList);
}
as #Saichi-Okuma said that to copy the content of a list you should use staticLawyersList = List.from(lawyersList) because in dart and most of the java compiler programming languages when you use staticLawyersList = lawyersList this means that you are referring to the lawyersList by the staticLawyersList.
then I manipulate the lawyersList as I want with help of staticLawyersList
lawyersList.clear();
lawyersList.addAll(staticLawyersList);
But when I did so, the consumer didn't apply the changes based on the staticLawyersList although the logcat shows that the staticLawyersList length is 10 which is what I want (full list without filtration).
the conclusion of my problem can be listed in two points:
1- the consumer is listening to only one list lawyersList and I think it still exists.
2- the pointer problem as #Saichi-Okuma mentioned.
here are the full code changes
void getFullListOfLawyers() {
lawyersList.clear(); // to make sure that the list is clean from older operations
lawyersList.addAll(staticLawyersList);// the trick
notifyListeners();
}
Future<List<Lawyer>> fetchLawyers() async{
final response = await get(Uri.parse(url.toString()));
if(response.statusCode == 200){
var dynamicLawyersList = jsonDecode(response.body);
print('$dynamicLawyersList');
lawyersList = List<Lawyer>.from(dynamicLawyersList.map((x) => Lawyer.fromJson(x)));
staticLawyersList = List.from(lawyersList);// use this statment instead of staticLawyersList = lawyersList
lawyersList.forEach((element) {print('all lawyers: ${element.data!.location}');});
notifyListeners();
return lawyersList;
}
else{
notifyListeners();
throw Exception(response.statusCode);
}
}
The Consumer Widget gets rebuild every time you call notify notifyListeners, regardless the state of any lists.
Maybe you are not accessing the Instance of the API being consumed. Make sure you are using the 2nd parameter of the Consumer builder.
Consumer<LawyerAPIServices>(builder: (context, lawyerAPI, child) =>
FutureBuilder(
future: lawyerAPI.fetchLawyers(),
builder: ((context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
if (setByPosition) {
//update UI with `lawyersList`
return _listView(snapshot.data!);
} else {
//update UI with `staticLawyersList`
// Since the Future state is Complete you can be sure that
// the staticLawyersList variable in your API was already set
return _listView(lawyerAPI.staticLawyersList);
}
case ConnectionState.none:
return const Text('Error');
default:
return const CircularProgressIndicator.adaptive();
}
}),
I don't think you need the code below for this particular need. It'd override your lawyersList and notify to all listeners even though nothing really changed. Just access your staticLawyersList directly, since it was populated when you called fetchLawyers.
void getFullListOfLawyers() {
lawyersList.clear(); // to make sure that the list is clean from older operations
lawyersList.addAll(staticLawyersList);// the trick
notifyListeners();
}

Delete map in a firestore table

I am having trouble deleting Maps in a data table in Firestore. Indeed, either I delete my entire array, or I receive an error of the type:
flutter: Failed to delete 1: Invalid argument: Instance of '_CompactLinkedHashSet '
I am attaching my classes to you so that you can understand better.Thank you in advance
CLASS Delete_description :
import 'package:cloud_firestore/cloud_firestore.dart';
class DeleteDescription {
final String city;
final String citee;
final int value;
CollectionReference cities = FirebaseFirestore.instance.collection('city');
DeleteDescription(this.city, this.citee, this.value) {
deleteDescription();
}
Future<void> deleteDescription() {
return cities
.doc(city)
.collection("citee")
.doc(citee)
.set({
"Description": FieldValue.arrayRemove([
{0}
])
})
.then((value) => print("$citee Deleted"))
.catchError((error) => print("Failed to delete $value: $error"));
}
}
CLASS READDESCRIPTION:
import 'package:ampc_93/fonction/firebase_crud/delete_description.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class ReadDescription extends StatefulWidget {
final String titreCity;
final String titreCitee;
ReadDescription(this.titreCity, this.titreCitee);
#override
_ReadDescriptionState createState() => _ReadDescriptionState();
}
class _ReadDescriptionState extends State<ReadDescription> {
#override
Widget build(BuildContext context) {
CollectionReference cities = FirebaseFirestore.instance.collection("city");
return FutureBuilder<DocumentSnapshot>(
future: cities
.doc(widget.titreCity)
.collection("citee")
.doc(widget.titreCitee)
.get(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.hasData && !snapshot.data!.exists) {
return Text("Documents does not exist");
}
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data!.data() as Map<String, dynamic>;
if (data["Description"] == null) {
return Text("");
} else {
return ListView.separated(
itemBuilder: (context, index) {
return ListTile(
title: Text(
data["Description"][index]["Identite"],
textAlign: TextAlign.justify,
),
subtitle: Text(
data["Description"][index]["Role"],
textAlign: TextAlign.justify,
style: TextStyle(
decoration: TextDecoration.underline,
color: Colors.red),
),
leading: Icon(Icons.person),
trailing: IconButton(
onPressed: () => DeleteDescription(
widget.titreCity, widget.titreCitee, index),
icon: Icon(Icons.delete_forever),
color: Colors.red[300],
));
},
separatorBuilder: (context, index) => Divider(),
itemCount: data["Description"].length);
}
}
return Text("Loading");
},
);
}
}
I specify that in my database, "Description" is an array and that I would therefore like to delete all the elements of "Description" number 0 for example
The FieldValue.arrayRemove you are using didn't work in this way. There are two methods to delete data from firestore list.
First way is pass element (Not it's index) in FieldValue.arrayRemove which you wants to delete.
Second way is get collection from firestore and modify data according to your need and update collection in firestore.
Have a look on below code for more understanding.
import 'package:cloud_firestore/cloud_firestore.dart';
class DeleteDescription {
final String city;
final String citee;
final int value;
CollectionReference cities = FirebaseFirestore.instance.collection('city');
DeleteDescription(this.city, this.citee, this.value) {
deleteDescription();
}
Future<void> deleteDescription() {
final snapshot = await cities.doc(city).collection("citee").doc(citee).get();
/* Get list from firestore */
final list = snapshot["Description"] as List;
/* Remove first or any element and delete from list */
list.removeAt(0);
/* Update same list in firestore*/
await cities
.doc(city)
.collection("citee")
.doc(citee)
.set({"Description": list}).then((value) => print(" Deleted"));
}
}
A helpful way to consider Firestore "Arrays" is that they are ABSOLUTELY NOT ARRAYS - they are ORDERED LISTS (ordered either by the order they were added to the array, or the order they were in in an array passed to the API as an array), and the "number" shown is the order, not an index. The only way to "identify" a single element in a Firestore Array[ordered list] is by it's exact and complete value. It is VERY unfortunate they chose the name "array".
That said, when you read a document, the result presented to your CODE is in the form of an array, and GAINS the ability to refer to an element by index - which is why you have to EITHER:
=> at the backend / API call, specify an element by "value", which in this case is the ENTIRE object on the list
OR
=> at the Client, read the document, delete the desired element either by index or value, then write the entire array back to the backend.