I have the following build method of a statefull widget. I am trying to test updating a Firebase collection stream dynamically using a ChangeNotifier. The code is the following:
#override
Widget build(BuildContext context) {
final List<String> types = ["pop", "ballad", ''];
final SongStreamNotifier songStreamNotifier =
Provider.of<SongStreamNotifier>(context);
developer.log("Rebuilding Scaffold");
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
types.shuffle();
final songType = types.first;
songStreamNotifier.getSongsStream(songType);
},
),
body: StreamBuilder<QuerySnapshot>(
stream: songStreamNotifier.songs,
builder: (context, snapshot) {
developer.log("Rebuilding stream");
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.size,
itemBuilder: (BuildContext ctx, int index) {
return Text(snapshot.data!.docs[index].get('name'));
});
} else {
return Container();
}
}),
);
}
Notifier is called correctly, I can see all log messages on how build is rerun, but the values do not change. The code of the ChangeNotifier is the following:
class SongStreamNotifier extends ChangeNotifier {
Stream<QuerySnapshot> stream = Stream.empty();
final SongService songService = SongService();
Stream<QuerySnapshot> get songs {
return stream;
}
getSongsStream(String type) {
developer.log('type: $type');
stream = songService.getSongs(type);
notifyListeners();
}
}
which is pretty straighforward: songService.getSongs filters the songs based on the songType.
I still cannot understand how providers work with updating the state of widgets. How come I see the builder method getting run but the StreamBuilder always fetches the same values?
Found what the issue was. I didn't call snapshots on the query but only on the initial collection reference. In my Repository i did this:
colRef = _db.collection("songs");
if (type != '') {
colRef.where('kind', isEqualTo: type);
}
return colRef.snapshots()
while I should have done
colRef = _db.collection("songs");
if (type != '') {
return colRef.where('kind', isEqualTo: type).snapshots();
}
return colRef.snapshots()
but I didn't know colRef was immutable.
Related
Currently struggling to make a ListView data retrieved from Firestore.
I am trying to get "kids name" saved under in the firestore as linked photo.
Firestore
No error message is shown up but the data is not retrieved correctly and shown blank screen...hope anyone can correct my code!
and here is my code:
class kidsNamePick extends StatefulWidget {
#override
_kidsNamePickState createState() => _kidsNamePickState();
}
class _kidsNamePickState extends State<kidsNamePick> {
List<Memo> kidsnamelist = [];
Future<void>fetchMemo()async{
final kidsnames = await FirebaseFirestore.instance.collection('useraccount').doc(FirebaseAuth.instance.currentUser!.uid)
.collection('kidsname').get();
final docs = kidsnames.docs;for (var doc in docs){
Memo fetchMemo = Memo(kidsname: doc.data()['kids name'],
);
kidsnamelist.add(fetchMemo);}
setState(() {
});}
#override
void initState(){
super.initState();
fetchMemo();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add/Select Kids'),
),
body: ListView.builder(
itemCount: kidsnamelist.length,
itemBuilder: (context, index){
return ListTile(
title: Text(kidsnamelist[index].kidsname),
);
},
)
);
}
}
The best way to call future method is using FutureBuilder, first change your fetchMemo to this:
Future<List<Memo>> fetchMemo() async {
try {
final kidsnames = await FirebaseFirestore.instance
.collection('useraccount')
.doc(FirebaseAuth.instance.currentUser!.uid)
.collection('kidsname')
.get();
final docs = kidsnames.docs;
return docs
.map((doc) => Memo(
kidsname: doc.data()['kids name'],
))
.toList();
} catch (e) {
return [];
}
}
then change your build method to this:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add/Select Kids'),
),
body: FutureBuilder<List<Memo>>(
future: fetchMemo(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
List<Memo> data = snapshot.data ?? [];
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].kidsname),
);
},
);
}
}
},
),
);
}
I have just created a demo for better understanding future builder
scaffold body showing all users from api and appear should be shown with number of users
appear's title showing 0 when loaded but does not change...what to do to rebuild it
here is my code
class _withmodelState extends State<withmodel> {
List<UserModel> userlist=[];
Future<List<UserModel>> getdata() async {
final resp =
await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (resp.statusCode == 200) {
print('i ma called');
List<dynamic> dlist = json.decode(resp.body);
await Future.delayed(Duration(seconds: 2));
userlist= dlist.map((e) => UserModel.fromJson(e)).toList();
return userlist;
}
return userlist;
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(title: Text("Total users="+userlist.length.toString()),),
body: MyBody(
//MyBody returning FutureBuilder for showing userlist array;
),
));
}
You can use ChangeNotifier like this, first create a class like this:
class WithmodelDecl with ChangeNotifier {
ValueNotifier<int> totalUsers = ValueNotifier<int>(0);
}
WithmodelDecl withmodeldecl = new WithmodelDecl();
then use it like this:
return SafeArea(
child: Scaffold(
appBar: PreferredSize(
child: ValueListenableBuilder<int>(
valueListenable: withmodeldecl.totalUsers,
builder: (context, value, _) {
return AppBar(
title: Text("Total users=" + value.toString()),
);
}),
preferredSize: AppBar().preferredSize),
body: MyBody(
//MyBody returning FutureBuilder for showing userlist array;
),
));
and finally change your getdata to this:
Future<List<UserModel>> getdata() async {
final resp =
await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (resp.statusCode == 200) {
print('i ma called');
List<dynamic> dlist = json.decode(resp.body);
await Future.delayed(Duration(seconds: 2));
userlist= dlist.map((e) => UserModel.fromJson(e)).toList();
withmodeldecl.totalUsers.value = userlist.length;
return userlist;
}
return userlist;
}
You also need to rebuild the Text widget, that you are using to show the count, when the count is available, i.e., the Future completes.
You need to wrap that Text widget with FutureBuilder like this:
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: FutureBuilder<List<UserModel>>(
future: getdata(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final List<UserModel> userlist = snapshot.data!;
return Text("Total users= ${userlist.length}");
// it's better to use String interpolation than "Total users=" + snapshot.data!.length.toString()
} else {
// return loading widget
}
},
),
),
body: MyBody(
//MyBody returning FutureBuilder for showing userlist array;
),
),
);
It is better to have the Future in a variable, and then use it like this, to avoid unwanted and repeated calling of it whenever the build() method is called:
late final Future<List<UserModel>> _userListFuture;
And initialize it in your initState(), like this:
#override
void initState() {
super.initState();
_userListFuture = Future<List<UserModel>>(getdata);
}
And use it with your FutureBuilder like this:
FutureBuilder<List<UserModel>>(
future: _userListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// return your widget showing data
} else {
// return loading widget
}
},
)
At first, when i started writing my calls to get data from firestore, it worked. But when i tried writing more docs to my collection, it failed to bring data for the docs i recently added. Then, when i deleted the first one i added, i stopped receiveing data from firestore all together. I have tried several methods, but have all ended in failure.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class collect extends StatefulWidget {
#override
_collectState createState() => _collectState();
}
class _collectState extends State<collect>
{
Future _data;
void initState()
{
super.initState();
_data = getStuff();
}
Future getStuff()
async {
var firestore = FirebaseFirestore.instance;
QuerySnapshot qn = await firestore.collection("buses").get();
return qn.docs;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _data,
builder: (_, snapshot)
{
if(snapshot.connectionState == ConnectionState.waiting)
{
return Center(
child:Text("Loading")
);
}
else if(snapshot.connectionState == ConnectionState.done)
{
return ListView.builder(itemCount: snapshot.data.length,itemBuilder:(_, index)
{
return Container(
child: ListTile(
title: Text(snapshot.data[index].data()["name"].toString()),
subtitle: Text(snapshot.data[index].data()["price"].toString()),
),
);
});
}
},
),
);
}
}
```![enter image description here](https://i.stack.imgur.com/L7FqF.jpg)
Define your database call as,
Future getStuff() async {
var docs;
await FirebaseFirestore.instance
.collection("buses")
.get()
.then((querySnapshot) {
docs = querySnapshot.docs;
});
return docs;
}
Then use the FutureBuilder in the build() function as,
return Scaffold(
body: Center(
child: FutureBuilder<dynamic>(
future: getStuff(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return Container(
child: ListTile(
title: Text(
snapshot.data[index].data()["name"].toString()),
subtitle: Text(
snapshot.data[index].data()["price"].toString()),
),
);
});
} else {
return CircularProgressIndicator();
}
},
),
),
);
I wrapped the FutureBuilder inside a Center just for clarity, you may remove that Center widget.
I'm trying to update my listview widget from BloC but it's always returning an empty snapshot.BLoC is using a stream from an async SQLite query using sqflite. The sqflite query appears to be working when accessed directly.
Here's a snippet of the UI portion:
ListBloc listBloc = ListBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(child: getBoxList()),
FlatButton(
onPressed: () {
listBloc.queryBoxes();
},
child: Text('pull data'),
),
],
),
),
);
}
Widget getBoxList() {
return StreamBuilder(
stream: listBloc.boxes,
builder: (BuildContext context, AsyncSnapshot<List<Box>> snapshot) {
return getList(snapshot);
});
}
Widget getList(AsyncSnapshot<List<Box>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return Text(snapshot.data[index].boxName);
},
);
} else {
return Text('snapshot has no data');
}
}
Hers's a snippet of the BloC:
class ListBloc {
final _boxListController = StreamController<List<Box>>.broadcast();
final DatabaseHelper _db = DatabaseHelper.instance;
get boxes => _boxListController.stream;
ListBloc() {
getAllIBoxes();
}
getAllIBoxes() async {
_boxListController.sink
.add(await _db.queryAllBoxRows(DatabaseHelper.tableBoxes));
}
dispose() {
// _itemListController.close();
_boxListController.close();
}
}
_db.queryAllBoxRows(DatabaseHelper.tableBoxes) works just fine when tested outside the BLoC.
Thank you!
-- update --
here's the queryBoxes function in BLoC. I just added this to test if the query returns data.
queryBoxes() async {
DatabaseHelper _db = DatabaseHelper.instance;
final _boxes = await _db.queryAllBoxRows(DatabaseHelper.tableBoxes);
_boxes.forEach((element) {
print(element.boxName);
});
}
-- update 2 --
Source of the stream:
getAllIBoxes() async {
_boxListController.sink
.add(await _db.queryAllBoxRows(DatabaseHelper.tableBoxes));
}
is _db.queryAllBoxRows(DatabaseHelper.tableBoxes) which is a method in my DatabaseHelper that looks like this.
Future<List<Box>> queryAllBoxRows(table) async {
Database db = await instance.database;
final _result = await db.query(table);
List<Box> box = _result.map((e) => Box.fromDatabaseJson(e)).toList();
return box;
}
You are just printing the values in the queryBoxes function. You have to inform your stream that there is data available, for that you've to add the values to the sink.
Modify your function to:
queryBoxes() async {
DatabaseHelper _db = DatabaseHelper.instance;
final _boxes = await _db.queryAllBoxRows(DatabaseHelper.tableBoxes);
_boxes.forEach((element) {
print(element.boxName);
});
_boxListController.sink.add(boxes);
}
I have a problem with my BLoC implementation, I have this code in synchronize.dart:
...
class _SynchronizeState extends State<Synchronize> {
UserBloc userBloc;
//final dbRef = FirebaseDatabase.instance.reference();
#override
Widget build(BuildContext context) {
userBloc = BlocProvider.of(context);
return Scaffold(
resizeToAvoidBottomPadding: false,
body: Container(
...
),
child: StreamBuilder(
stream: dbRef.child('info_tekax').limitToLast(10).onValue,
builder: (context, snapshot) {
if(snapshot.hasData && !snapshot.hasError){
Map data = snapshot.data.snapshot.value;
List keys = [];
data.forEach( (index, data) => keys.add(index) );
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => SynchronizeItem(title: keys[index], bottom: 10, onPressed: (){ print(keys[index]); })
);
}else{
return Container(
child: Center(
child: Text('Loading...'),
),
);
}
}
),
),
);
}
}
The previos code, works correctly, but i want implemente bloc Pattern, i have userBloc then i want to put this
userBloc.getDevicesForSinchronized()
instead of
dbRef.child('info_tekax').limitToLast(10).onValue,
my problem is this:
void getDevicesForSynchronized() {
return dbRef.child(DEVICES).limitToLast(10).onValue;
}
i get this error **A vaue of type 'Stream' can't be returned from method 'getDevicesForSynchronized' because it has a return type of 'void'
The error is very clear, but i don't know what is type that i need return, try:
Furure<void> getDevicesForSynchronized() async {
return await dbRef.child(DEVICES).limitToLast(10).onValue;
}
or
Furure<void> getDevicesForSynchronized() async {
dynamic result = await dbRef.child(DEVICES).limitToLast(10).onValue;
}
and another solutions, but I don't know how return correctly value for use in the StreamBuilder
From the error message you can see that the return type is Stream. Change your method like:
Future<Stream> getDevicesForSynchronized() async {
return dbRef.child(DEVICES).limitToLast(10).onValue;
}