I have a TODO List function (Alarmas), but I feel I'm not taking advantage of Firebase's Realtime features enough.
The Widget displays the list very well, however when someone puts a new task from another cell phone, I am not being able to show it automatically, but I must call the build again by clicking on the "TODO button" in the BottomNavigationBar.
Is there a way that the new tasks are automatically displayed without doing anything?
I'm using streams to get the list...
#override
Widget build(BuildContext context) {
alarmaBloc.cargarAlarmas();
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
return ListView(
children: [
for (var itemPendiente in tareasList)
_crearItem(context, alarmaBloc, itemPendiente),
//more widgets
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
And, I read the Firebase Data in this way...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp); // element.snapshot.value.
});
await resp.once().then((snapshot) {
print("Total list was loaded - ${alarmaList.length}");
}); //I'm using this await to be sure that the full list was loaded, so I can order and process it later
return alarmaList;
}
How can I display a List from Firebase in "true" Real Time?
To properly manage the state of asynchronously loaded data, you should:
Start loading/listening to the data in initState()
Set the data into the state (with setState()) when you receive it or it is updated.
Then render it from the state in the build method.
So in your code that'd be something like:
final List<AlarmaModel> alarmaList = new List(); // this is now a field in the `State` object
#override
void initState() {
super.initState();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp);
setState(() {
alarmaList = alarmaList;
})
});
}
#override
Widget build(BuildContext context) {
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
If you only want to repaint once you've gotten a complete update from the database, you can put the call to setState() in a value listener, just use onValue in that instead of once(), as you also want to catch the updates.
Related
I'm making my first Flutter app and I encounter a problem and doesn't found any solution for it.
I have a view where I render a Firestore document, and there is two ways of getting there:
From a list where I already loaded my documents
From Dynamic Links with uid attached as arguments (args)
So in order to listen document changes and loading the data when arriving from the link I used FirestoreBuilder like this:
return FirestoreBuilder<EventDocumentSnapshot>(
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<EventDocumentSnapshot> snapshot, Widget? child) {
if (!snapshot.hasData) {
return Container();
}
Event? event = snapshot.requireData.data;
return Scafold(); //Rest of my rendering code
}
);
How I could avoid first call to Firebase when I already have the data but still listen to changes? The main problem is that my hero animation doesn't work because of this.
I tried with a StreamBuilder and initialDataparam but since it's expecting stream I didn't know how to cast my data.
Okay, so I found the solution myself after many tries, so I added my Model object that can be null as initialData, but the thing that makes me struggle with is how you get the data in the builder. You have to call different methods depending on where the data is coming from.
return StreamBuilder(
initialData: args.event
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
// Here is the trick, when data is coming from initialData you only
// need to call requireData to get your Model
Event event = snapshot.requireData is EventDocumentSnapshot ? snapshot.requireData.data : snapshot.requireData;
return Scafold(); //Rest of my rendering code
}
);
Reading through cloud_firestore's documentation you can see that a Stream from a Query can be obtained via snapshots()
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
},
);
This won't help you, but with GetX it's simple to implement like this: You don't need StreamBuilder anymore.
//GetXcontroller
class pageController extends GetXcontroller {
...
RxList<EventModel> events = RxList<EventModel>([]);
Stream<List<EventModel>> eventStream(User? firebaseUser) =>
FirebaseFirestore.instance
.collection('events')
.snapshots()
.map((query) =>
query.docs.map((item) => UserModel.fromMap(item)).toList());
#override
void onReady() async {
super.onReady();
events.bindStream(
eventStream(controller.firebaseUser)); // subscribe any change of events collection
}
#override
onClose() {
super.onClose();
events.close(); //close rxObject to stop stream
}
...
}
You can use document snapshots on StreamBuilder.stream. You might want to abstract the call to firebase and map it to an entity you defined.
MyEntity fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snap) {
final data = snap.data()!;
return MyEntity (
id: snap.id,
name: data['name'],
);
}
Stream<MyEntity> streamEntity(String uid) {
return firebaseCollection
.doc(uid)
.snapshots()
.map((snapshot) => fromSnapshot(snapshot));
}
return StreamBuilder<MyEntity>(
// you can use firebaseCollection.doc(uid).snapshots() directly
stream: streamEntity(args.uid),
builder: (context, snapshot) {
if (snapshot.hasData) {
// do something with snapshot.data
return Scaffold(...);
} else {
// e.g. return progress indicator if there is no data
return Center(child: CircularProgressIndicator());
}
},
);
For more complex data models you might want to look at simple state management or patterns such as BLoC.
I had used stateful Widget for this page.
First I run the function in initState to store data into variable as List and then I output the data in the form of listview.
Also I had used async and await for the function in the initState.
I am using firebase to load data into by flutter app.
So the content of the page is loaded when I touch the screen, before touching the screen it is just black blank screen. But as soon as I touch the screen, suddenly the data is loaded in the form of ListView. So I want that data should loaded even without first touching the screen.
So any one can explain me that why this is happening, and how to stop this. I want to load data as soon as the data is downloaded and processed in background.
code:-
void initState(){
super.initState();
findFollowers(); }
findFollowers({bool clearCachedData = false}) async
{
if(clearCachedData) {
setState(() {
followersResults = null;
});
}
List<UserResult> searchFollowersResult = [];
QuerySnapshot allFollowers = await followersRefrence.doc(widget.userProfileId).collection("userFollowers").limit(limit).get();
allFollowers.docs.forEach((document) async {
QuerySnapshot ds = await usersReference.where("id", isEqualTo: document.id).get();
ds.docs.forEach((docu) {
User eachUser = User.fromDocument(docu);
UserResult userResult = UserResult(eachUser);
searchFollowersResult.add(userResult);
});
});
setState(() {
followersResults = searchFollowersResult;
if(allFollowers!=null)
{
if(allFollowers.docs.length != 0)
{
startAfter = allFollowers.docs[allFollowers.docs.length-1];
}
}
});
}
return ListView(children: followersResults, controller: scrollController,
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),);
A good approach to asynchronously reading and displaying data is to use FutureBuilder with ListView.builder, e.g.
Widget build(BuildContext context) {
log.v('build called');
final postService = Provider.of<PostService>(context);
return FutureBuilder(
future: postService.getPosts(),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
final List<Post> posts = snapshot.data;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
ProjectModel post = posts.data[index];
return Column(
Here the postService.getPosts() is an async method to read and return a list of posts. The FutureBuilder invokes its builder with the state of the access, we display a progress indicator while it is loading, then display a ListView when it is loaded. Note that ListView.builder is a more efficient way of rendering widgets in a scrollable list.
I have a collection in Firestore. It has a field array i am trying to get the array from Firestore and assign it to a list in flutter.
My collection is as below
My code for getting data from Firestore
List<Offset> pointlist = <Offset>[];
getdata() async{
await Firestore.instance.collection("points").document('biZV7cepFJA8T6FTcF08').get().then((value){
setState(() {
List<Offset> pointlist = List.from(value.data['point']);
});
});
}
#override
void initState() {
super.initState();
getdata();
}
i get this error type 'String' is not a subtype of type 'Offset'
The thing which you are doing wrong is this:
// You have initialised your List as a Offset Object Type
List<Offset> pointList;
Secondly, the data you are assigning is a String, if you closely take a look at that firebase.
"Offset(x,y)"
Finally, trying to assign the String value to a List of type Offset class/object
If you want to make the thing works, then either make the List of type String and then add it to the List
List<String> pointlist = List.from(value.data['point']);
Or first Add the data to the Offset Object like this, and then pass it to the List
List<Offset> pointList = <Offset>[];
getdata() async{
await Firestore.instance.collection("points").document('biZV7cepFJA8T6FTcF08').get().then((value){
setState(() {
// first add the data to the Offset object
List.from(value.data['point']).forEach((element){
Offset data = new Offset(element);
//then add the data to the List<Offset>, now we have a type Offset
pointList.add(data);
});
});
});
}
SUMMARY
Always look for the data type you are giving to the List, if you are trying to add the data which is not a type T of List<T>, you will always get this error of type mismatch. Hope this will give you some clarity and some basic idea about programming. Keep learning :)
you have to declare list and all list item will be store in declared list so you can access it.
List<dynamic> alldata =[];
Future<QuerySnapshot?> getData() async {
dataofItem = FirebaseFirestore.instance
.collection('$data')
.get()
.then((QuerySnapshot? querySnapshot) {
querySnapshot!.docs.forEach((doc) {
allData = doc["item_text_"];
print("allData = $allData");
// print("getData = ${doc["item_text_"]}");
});
});
return dataofItem;
}
Lets assume the structure to be.
...
arrayOfString
|_element1
|_element2
|_element3
...
Use the following code to get the array elements of arrayOfString parameter
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
Map<String, dynamic> data =
snapshot.data!.data()! as Map<String, dynamic>;
return ListView(
children: data['arrayOfString'].map<Widget>((e) { 👈 arrayOfString has array of Strings
return ListTile(
title: Text(e.toString()), // 👈 printing every string
);
}).toList());
},
),
),
);
}
I am trying to make my Flutter app update when a change is made to the usersCollection.document(user.uid) firebase document.
When the user document is updated I want to retrieve the data from this document but also from another firebase document, facilitiesCollection.document(...).
My current code
Future<Map> _getCheckedInFacilityData() async {
Map<String, dynamic> result = {};
try {
DocumentSnapshot userDoc =
await _db.usersCollection.document(user.uid).get();
if (userDoc.data['checkedIn']) {
// User is checked in
DocumentSnapshot facDoc = await _db.facilitiesCollection
.document(userDoc.data['activeFacilityID'].toString())
.get();
result['facilityID'] = userDoc.data['activeFacilityID'];
result['sessionID'] = userDoc.data['activeSessionID'];
result['facilityActiveUsers'] = facDoc.data['activeUsers'].length;
result['facilityName'] = facDoc.data['name'];
return result;
}
} catch (er) {
debugPrint(er.toString());
}
return null;
}
FutureBuilder<Map>(
future: _getCheckedInFacilityData(),
builder: (context, map) {
switch (map.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
...
This is currently working but the page is not updated when a change is made to the user document.
I haven't been using Flutter/Dart for long so any ideas are welcome.
Is it possible to return a custom object/map which is comprised of 2 separate documents from a StreamBuilder, or is there another method that will work in my situation.
Surely you can do it with Streams asyncMap() and then listen in StreamBuilder
Basic algoritm
Get stream of you first data type and then asyncMap to wait second data type and return them both
stream.asyncMap(
(v1) async {
final v2 = await Future.delayed(Duration(seconds: 1), () => 4);
return v1 * v2;
},
);
Closer to your code
Stream<Map<String, dynamic>> _getCheckedInFacilityData() {
return _db.usersCollection.document(user.uid).snapshots()
.asyncMap(
(userDoc) async {
final DocumentSnapshot facDoc =
await _db.facilitiesCollection
.document(userDoc.data['activeFacilityID'].toString())
.get();
final Map<String, dynamic> userMap = userDoc.data;
final Map<String, dynamic> facMap = facDoc.data;
return userMap..addAll(facMap);
},
);
}
In this function I merge two maps - be carefull if both maps have identical keys map will keep only last was added key in our case from addAll(facMap)
Last step is to show you streamed data on screen - use StreamBuilder
StreamBuilder<Map>(
stream: _getCheckedInFacilityData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('${snapshot.error}');
} else if (snapshot.connectionState == ConnectionState.waiting) {
return LinearProgressIndicator();
}
return /* some widget that shows your data*/;
},
),
I am getting an error that says that the method .length is calling on a null object _genreList.
I am using an async method to get data from a local asset sqlite database to which is a list of genre's. Which then I use ListView.builder in order to display that list on the screen. This is the code to obtain the data...
Future getGenreData() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "asset_sample_sqlite.db");
ByteData data = await rootBundle.load(join("assets", "sample_sqlite.db"));
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await new File(path).writeAsBytes(bytes);
Database db = await openDatabase(path);
_genreList = await db.rawQuery('SELECT genre_name[] FROM tbl_genres');
print(_genreList);
await db.close();
}
How do I use this method inside the build Widget method so that I can access the _genreList when I use ListView.builder? like so..
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: _genreList.length, //need to access the genreList here
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new ListTile(
title: new Text("${_genreList[index]}"),
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => BookPage(id: index),
),
);
}
),
);
}
),
);
}
The end goal here is to display a list of genres (from the tbl_genres in my sqlite database) that will be able to pass through data to the next page to show a list of books (from the tbl_books in my sqlite database) related to that genre.
The whole point of programming asynchronously is that your user interface can stay alive while you are doing time consuming work in the background. So you need (and want) to display something like a CircularProgressIndicator or even a blank page (e.g. a Container), while the application is loading.
There are at least these two ways of doing that:
Make the widget stateful and introduce a state field loading, that you initialize to true and set to false when your data (in another field) is ready. Your code would look like that:
import 'package:flutter/material.dart';
class GenresPage extends StatefulWidget {
#override
_GenresPageState createState() => _GenresPageState();
}
class _GenresPageState extends State<GenresPage> {
bool loading;
List<String> genreNames;
#override
void initState() {
super.initState();
loading = true;
getGenreData();
}
Future getGenreData() async {
final genreData = await actuallyGetThoseNames();
setState(() {
genreNames = genreData;
loading = false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: !loading ? new ListView.builder(
itemCount: genreNames.length,
itemBuilder: (context, index) {
return new Card(
child: new ListTile(
title: new Text("${genreNames[index]}"),
),
);
},
) : CircularProgressIndicator(), // or Container()
);
}
}
Use a FutureBuilder. Therefore you would need to refactor your getGenreData method to return the list as a Future<List<String>>.