I'm trying to write full app for the first time as a beginner. I'm using getx in this app too.
I'm taking a split second RangeError (index): Invalid value: Valid value range is empty: 0 while using StreamBuilder. It is working fine after that split second red error page. How I am supposed to deal with this, should I use something related with getx to stream the data?
This is my StreamBuilder:
StreamBuilder(
stream: stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasData) {
List<KTCardItem> collectionList = filterByTag(snapshot.data!.docs);
return PageView.builder(
controller: PageController(keepPage: true),
scrollDirection: Axis.vertical,
itemCount: collectionList.length,
itemBuilder: (BuildContext context, index) {
Future.delayed(const Duration(seconds: 3));
return NFTCardView(
index: index,
isFavorite: isFavoritedByUser(index),
ktCardItem: collectionList[index],
onFavChanged: () {
onFavoriteChanged(collectionList[index].eventId ?? "", index);
},
);
},
);
} else {
return const Center(child: CircularProgressIndicator());
}
// filter the list by choice of tag
},
);
I'm guessing that cause of this problem is the filterByTag method that I do before the return. filterByTag method:
List<KTCardItem> filterByTag(List<QueryDocumentSnapshot> snapshot) {
List<KTCardItem> collectionList = [];
for (var document in snapshot) {
Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
if (data["tags"].contains(_tag.value)) {
collectionList.add(KTCardItem.fromMap(data));
}
}
return collectionList;
}
Related
I have a function in which I add StreamBuilder(s) into a List, then I load these StreamBuilder(s) inside a Column, this approach is really memory consuming as all of the List is loaded at once, is there a way to do this in another way?
My showList function:
showList() {
List<Widget> theList= [];
for (CountryModel country in _allCountries) {
theList.add(StreamBuilder(
stream:FirebaseFirestore.instance.collection('user').doc(country.Id).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
xModel user = UserModel.fromDoc(snapshot.data);
return CountryWidget(
country: country,
viewer: user.id as String,);
} else {
return SizedBox.shrink();
}
}));
}
return theList;
}
In my Scaffold I have a Column where I use the spread operator to load a list into children[].
Column(
children: <Widget>[
...showList()
],
),
Try the following code:
ListView.builder(
itemCount: _allCountries.length + _anotherList.length,
itemBuilder: (context, index) {
if (index >= _allCountries != true) {
final CountryModel country = _allCountries[index];
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('user').doc(country.Id).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
xModel user = UserModel.fromDoc(snapshot.data);
return CountryWidget(
country: country,
viewer: user.id as String,
);
} else {
return SizedBox.shrink();
}
}
final item = _anotherList[index];
return widget;
},
);
},
),
I'm trying to create a user feed, just like that of twitter using Firebase & GetX.
In the code snippet is my function..
List<PostModel> postListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((doc) {
return PostModel(
id: doc.id,
text: (doc.data() as dynamic)["text"] ?? "",
creator: (doc.data() as dynamic)["creator"] ?? "",
timestamp: (doc.data() as dynamic)["timestamp"] ?? 0,
);
}).toList();
}
Future<List<PostModel>> getFeed() async {
List<String> usersFollowing = await UserService() //['uid1', 'uid2']
.getUserFollowing(FirebaseAuth.instance.currentUser!.uid);
QuerySnapshot querySnapshot = await FirebaseFirestore.instance.collection("posts").where('creator', whereIn: usersFollowing)
.orderBy('timestamp', descending: true)
.get();
return postListFromSnapshot(querySnapshot);
}
What I want to do is to display the Future function getFeed(), I'm using GetX for state management. So, my problem is how can I display the result of this function using a ListView.Builder()
Here's how I used the Future builder
FutureBuilder(
future: _.listPost,
initialData: [PostModel(id: "2", creator: "Fm", text: "Testing", timestamp: Timestamp.now())],
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasData == null){
return Text("Data is available");
} else{
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.toString().length,
itemBuilder: (context, index){
PostModel posts = snapshot.data[index];
return Column(
children: [
Text(posts.text)
],
);
},
);
}
},
)
And here's the error I got
The following NoSuchMethodError was thrown building:
The method '[]' was called on null.
Receiver: null
Tried calling: [](3)
It also pointed to an error on the
PostModel post line.. the [index] to be precise
First, make your AsyncSnapshot snapshot an AsyncSnapshot<List<PostModel>> snapshot. That is not your primary problem, but it will make things a lot easier to have proper typing and not have to guess around using dynamic.
Your problem is that hasData is a bool. It is either true or false, but never null. I wonder how you got that line past your compiler. Are you using an outdated version of Flutter? You should check this, your compiler is your friend, if it isn't helping you properly, this will be a hard and rocky road.
Anyway, you should check whether there is data, if there is none, you are still waiting:
FutureBuilder(
future: _.listPost,
builder: (BuildContext context, AsyncSnapshot<List<PostModel>> snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
} else {
final postList = snapShot.requireData;
return ListView.builder(
shrinkWrap: true,
itemCount: postList .length,
itemBuilder: (context, index){
final post = postList[index];
return Column(
children: [
Text(post.text)
],
);
},
);
}
},
)
My Goal
I want to iterate through one collection containing 1…n documents. I want to put the content of the documents in in ListView. The collection represents the ListView and each document should be one ListTile.
My Firestore Data
I have a Firestore database containing one collection called “current_question” containing multiple documents. Each document contains the details about one question: title, description, date and so on.
My doing so far
I know how to show the data of one document. What I don’t know is, how to load all documents and publish them.
The code for loading one document:
body: FutureBuilder(
future: connectToFirebase(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return StreamBuilder<DocumentSnapshot>(
stream: database.getQuestions(),
builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
Map<String, dynamic> items = snapshot.data.data;
print(items);
return ListView.builder(
itemCount: 1,
itemBuilder: (context, i) {
return ListTile(
title: Text(items['Titel'].toString()),
trailing: Text(items['Zaehler'].toString()),
);
}
);
}
},
);
}
}),
Future<void> connectToFirebase() async {
final FirebaseAuth auth = FirebaseAuth.instance;
AuthResult result = await auth.signInAnonymously();
user = result.user;
database = DatabaseService();
}
final CollectionReference aktFragen = Firestore.instance.collection('aktFragen')/*.orderBy('Zaehler')*/;
Stream getQuestions() {
return aktFragen.document('xAGRoZCgiClrpeAPtb5B').snapshots();
}
See the result in the screenshot:
enter image description here
If you want to retrieve all documents then you will have to do a QuerySnapshot instead. Here is one way of doing it:
// Creating an instance of your model with all the users info
List<CustomModelNameHere> _myListSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return CustomModelNameHere(
title: doc.data["title"] ?? "",
description: doc.data["description"] ?? "",
);
}).toList();
}
// Get the stream
Stream<List<CustomModelNameHere>> get getQuestions {
return aktFragen.snapshots()
.map(_myListSnapshot);
}
Great answer by #Unbreachable!
This is how I implemented it:
#override
Widget build(BuildContext context) {
return new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('currentQuestions').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return new ListView(
children: snapshot.data.documents.map((document) {
return new ListTile(
title: new Text(document['title'].toString()),
subtitle: new Text(document['count'].toString())
);
}).toList()
);
}
}
);
}
I also found a good youTube tutorial for this topic:
https://www.youtube.com/watch?v=Ak_6_pBBe3U&feature=youtu.be
I want to build cards on the basis of data received from the future which returns a map. Since cardDetails is being fetched from the backend, it requires some time but while building the cards using ListView.builder, it reaches to itemCount before the data is fetched which makes cardDetails to be null. If I hardcode the value of itemCount then, the error disappears and I get cards as required. Any clues on how to solve this issue would be helpful.
Update: It is going into the snapshot.hasError condition but I'm not able to figure out which error is it
In UI
if (_localStorageService.getStringFromLocalStorage() != 'testFalse')
FutureBuilder(
future: _localStorageService.getMapFromLocalStorage(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
cardDetails = snapshot.data;
return ListView.builder(
itemBuilder: (context, index) {
print("Shared Pref hasData");
return cardDetails == null
? CircularProgressIndicator()
: HomepageCards(
user: widget.user,
cardDetails: cardDetails[
cardDetails.keys.toList()[index]],
);
},
// verify if cardDetails is null to prevent app crash
itemCount:
(cardDetails == null ? 0 : cardDetails.keys.length),
scrollDirection: Axis.vertical,
controller: _controller,
shrinkWrap: true,
);
} else if (snapshot.hasError) {
// TODO: Shimmer skeleton
}
return CircularProgressIndicator();
},
)
else
StreamBuilder<DocumentSnapshot>(
stream: Firestore()
.collection('homepage')
.document(widget.user.uid)
.collection('h')
.document('28032020')
.snapshots(),
builder: (context, snapshot) {
if (snapshot.data != null) {
cardDetails = {};
snapshot.data.data.forEach((index, individualDetail) {
cardDetails[index] = individualDetail;
});
_localStorageService
.storeCardInSharedPreference(cardDetails);
cardDetailKeys = snapshot.data.data.keys;
} else if (snapshot.hasError) {
// TODO: Show skeletal shimmer
} else {
// TODO: Convert it to Shimmer with card skeletal layout
CircularProgressIndicator();
}
return cardDetails == null
? CircularProgressIndicator()
: ListView.builder(
itemBuilder: (context, index) {
return HomepageCards(
user: widget.user,
cardDetails:
cardDetails[cardDetails.keys.toList()[index]],
);
},
itemCount: (cardDetailKeys == null
? 0
: cardDetailKeys.length),
scrollDirection: Axis.vertical,
controller: _controller,
shrinkWrap: true,
);
},
)
LocalStorage Service for Shared Preferences
class LocalStorageService {
static SharedPreferences _sharedPreferences;
final String screenkey;
String value;
String _initialSharedValue;
LocalStorageService({#required this.screenkey});
initialiseLocalStorage() async {
_sharedPreferences = await SharedPreferences.getInstance();
persist(screenkey);
}
Future<void> persist(String key) async {
_initialSharedValue = _sharedPreferences?.getString(key);
// will be null if never previously saved
if (_initialSharedValue == null) {
_initialSharedValue = 'testFalse';
}
await _sharedPreferences?.setString(screenkey, _initialSharedValue);
print("share = ${_sharedPreferences?.getString(screenkey)}");
}
storeCardInSharedPreference(Map cardDetails) async {
await _sharedPreferences?.setString(screenkey, json.encode(cardDetails));
}
getMapFromLocalStorage() async {
return await json.decode(_sharedPreferences?.getString(screenkey));
}
String getStringFromLocalStorage() {
return _sharedPreferences?.getString(screenkey);
}
}
This is because regardless of the status of your futurebuilder, Listview is being returned.
If you want to control the status of your futurebuilder, you must put the return inside your if/else/case.
Thus:
FutureBuilder(
future: _localStorageService.getStringFromLocalStorage(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
cardDetails = snapshot.data;
print("number of cards = ${cardDetails.keys.length}");
return ListView.builder(
itemBuilder: (context, index) {
print("card details in futute : ${snapshot.data}");
return cardDetails == null
? CircularProgressIndicator()
: HomepageCards(
user: widget.user,
cardDetails:
cardDetails[cardDetails.keys.toList()[index]],
);
},
// verify if cardDetails is null to prevent app crash
itemCount: (cardDetails == null? 0: cardDetails.keys.length),
scrollDirection: Axis.vertical,
controller: _controller,
shrinkWrap: true,
);
} else if (snapshot.hasError) {
print("Error here in snapshot");
return Center(child:Text("An error has occurred"));
} else {
return CircularProgressIndicator();
}
},
)
I have a page which displays 2 elements, both of them are different StreamBuilder but the second one depends on the first one.
To make it more clear I display this:
Firebase documents (list)
Firebase user
If we sign out both StreamBuilder disappear. That's fine, but my problem comes when I need to select a document from the list:
return ListTile(
leading: FlutterLogo(size: 40.0),
title: Text(set["title"]),
selected: _selected[index],
trailing: Badge(
badgeColor: Colors.grey,
shape: BadgeShape.circle,
toAnimate: true,
onTap: () => setState(() => _selected[index] = !_selected[index]),
);
Everytime I do the SetState() I refresh the first StreamBuilder (not sure why) and with this the second one.
This is the list widget:
Widget _mySetsLists(BuildContext context) {
List<bool> _selected = List.generate(20, (i) => false);
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
FirebaseUser user = snapshot.data;
if (snapshot.hasData) {
return StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.collection('sets')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot set = snapshot.data.documents[index];
return ListTile(
leading: FlutterLogo(size: 40.0),
title: Text(set["title"]),
selected: _selected[index],
onTap: () => setState(() => _selected[index] = !_selected[index]),
);
},
);
} else {
return Center(
child: new CircularProgressIndicator(),
);
}
},
);
} else {
return Text("loadin");
}
},
);
}
}
And this is the user profile:
class UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
return SliverList(
delegate: SliverChildListDelegate(
[
_mySetsLists(context),
Divider(),
StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
FirebaseUser user = snapshot.data;
if (user == null) {
return Text('not logged in');
}
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(
user.photoUrl,
),
),
title: Text(user.displayName),
subtitle: Text(user.email),
trailing: new IconButton(
icon: new Icon(Icons.exit_to_app),
highlightColor: Colors.pink,
onPressed: () {
authService.signOut();
}),
);
} else {
return Text("loading profile"); // <---- THIS IS WHAT I SEE
}
},
),
],
),
);
}
I also went through the same difficulty, but this is the trick i used
var itemsData = List<dynamic>();
var _documents = List<DocumentSnapshot>();
#override
void initState() {
// TODO: implement initState
super.initState();
getData();
}
getData(){
Firestore.instance
.collection('users')
.document(currentUser.uid)
.collection('set')
.getDocuments()
.then((value) {
value.documents.forEach((result) {
setState(() {
_documents.add(result);
itemsData.add(result.data);
});
});
});
}
replace your listview builder will be like this
ListView.builder(
shrinkWrap: true,
itemCount: _documents.length,
itemBuilder: (context, index) {
return ListTile(
title:Text(itemsData[index]['name'])
)
})
Hope it helps!!
If you pretend to use setstat a lot using the stream you can download the data locally. So every reload will not download data again, but just show the local data.
First step: declare the variable that will store data locally.
QuerySnapshot? querySnapshotGlobal;
Then where you read the streamData, first check if the local data you just declared is empty:
//check if its empty
if(querySnapshotGlobal==null)
//as its empty, we will download it from firestore
StreamBuilder<QuerySnapshot>(
stream: _queryAlunos.snapshots(),
builder: (context, stream){
if (stream.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
else if (stream.hasError) {
return Center(child: Text(stream.error.toString()));
}
else if(stream.connectionState == ConnectionState.active){
//QuerySnapshot? querySnapshot = stream.data;
//instead of save data here, lets save it in the variable we declared
querySnapshotGlobal = stream.data;
return querySnapshotGlobal!.size == 0
? Center(child: Text('Sem alunos nesta turma'),)
: Expanded(
child: ListView.builder(
itemCount: querySnapshotGlobal!.size,
itemBuilder: (context, index){
Map<String, dynamic> map = querySnapshotGlobal!.docs[index].data();
//let it build
return _listDeAlunoswid(map, querySnapshotGlobal!.docs[index].id);
},
),
);
}
return CircularProgressIndicator();
},
)
else
//now, if you call setstate, as the variable with the data is not empty, will call it from here e instead of download it again from firestore, will load the local data
Expanded(
child: ListView.builder(
itemCount: querySnapshotGlobal!.size,
itemBuilder: (context, index){
Map<String, dynamic> map = querySnapshotGlobal!.docs[index].data();
return _listDeAlunoswid(map, querySnapshotGlobal!.docs[index].id);
},
),
),
Hope it helps you save some money!