Flutter: Future keeps rebuilding because it is being called inside a stream. How to only make it call again, if possible? - flutter

So I wanted to display a list of songs but the future that displays a Uint8List artwork of the songs is called from a future. The code works but the album art looks as if it is glitching because it is constantly being called. I had not idea how to fix this and I have tried many solutions. Please help.
Here is my code:
StreamBuilder<List<SongInfo>>(
stream: widget.songs,
builder: (context, snapshot) {
if (snapshot.hasError)
return Utility.createDefaultInfoWidget(Text("${snapshot.error}"));
if (!snapshot.hasData)
return Utility.createDefaultInfoWidget(
CircularProgressIndicator());
return (snapshot.data.isEmpty)
? NoDataWidget(
title: "There is no Songs",
)
: Column(
children: [
Container(
padding:
EdgeInsets.symmetric(vertical: 10, horizontal: 15),
alignment: Alignment.centerRight,
child: Text("${snapshot.data.length} Songs"),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (context, songIndex) {
SongInfo song = snapshot.data[songIndex];
return ListItemWidget(
title: Text("${song.title}"),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: <Widget>[
Text("Artist: ${song.artist}"),
Text(
"Duration: ${Utility.parseToMinutesSeconds(int.parse(song.duration))}",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500),
),
],
),
trailing: (widget.addToPlaylistAction == true)
? IconButton(
icon: Icon(Icons.playlist_add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(_dialogTitle),
content: FutureBuilder<
List<PlaylistInfo>>(
future: model.getPlayList(),
builder:
(context, snapshot) {
if (snapshot.hasError) {
print("has error");
return Utility
.createDefaultInfoWidget(
Text(
"${snapshot.error}"));
}
if (snapshot.hasData) {
if (snapshot
.data.isEmpty) {
print("is Empty");
return NoDataWidget(
title:
"There is no playlists",
);
}
return PlaylistDialogContent(
options: snapshot.data
.map((playlist) =>
playlist.name)
.toList(),
onSelected: (index) {
snapshot.data[index]
.addSong(
song: song);
Navigator.pop(
context);
},
);
}
print("has no data");
return Utility
.createDefaultInfoWidget(
CircularProgressIndicator());
}),
);
});
},
tooltip: "Add to playlist",
)
: Container(
width: .0,
height: .0,
),
leading: song.albumArtwork == null
? FutureBuilder<Uint8List>(
future: model.audioQuery.getArtwork(
type: ResourceType.SONG,
id: song.id,
size: Size(100, 100)),
builder: (context, snapshot) {
SchedulerBinding.instance
.addPostFrameCallback(
(_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
if (snapshot.data.isEmpty)
return CircleAvatar(
backgroundImage: AssetImage(
"assets/images/title.png"),
);
if (isDataFetched) {
return CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: MemoryImage(
snapshot.data,
),
);
} else {
return CircleAvatar(
child: CircularProgressIndicator(),
);
}
})
: CircleAvatar(
backgroundImage: FileImage(
IO.File(song?.albumArtwork)),
),
);
},
),
),
],
);
},

I would prefer not to set state inside future builders or stream builders using post-frame callbacks. The reason being you basically asking flutter to build the widget again in the next frame while building the current one which recursively sets the whole thing in a loop. Maybe you can create a new stateful widget and do the loading task manually inside the initState if you need those isServiceError and isDataFetched flags.
The problem in your current code seems to be related to:
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Which is called inside the future builder. Everytime you set the state, the same code is called again as the widget is rebuilt thus forming a loop in which the whole thing is built again and again needlessly.
You can avoid it by checking the flags before assigning a post-frame callback like so:
if(!isDataFetched)
{
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isDataFetched = true;
}));
}
So in the next frame, isDataFetched will be true hence no further post-frame callbacks.
This solution however is not really a proper solution because as I mentioned above, it's not a good idea to set the state in future builders using post-frame callbacks. If you don't need those flags outside the future builder, you should simply avoid them and rely on snapshot.hasData and snapshot.hasError inside the builder itself.

Related

Data From multiple FutureBuilders in flutter

I'm fetching data from an api source , the data is fetched properly , then i store the data in sqflite , so basically after doing both , i need to check if there is connection so that i show data from internet other than that i get data back from database , now since i'm using futurebuilder which return internet async operation result , how would i be also to get list of data from database , any help is appreciated guys and thank you in advance.
This is what i have tried so far
#override
void initState() {
super.initState();
dbHelper = DbHelper();
}
#override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text("News Application"),
centerTitle: true,
backgroundColor: Colors.black,
titleTextStyle: TextStyle(color: Colors.white),
),
body: FutureBuilder (
future: Future.wait([getEverything(),dbHelper.getAllNews()]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if(snapshot.hasError) {
// So basically here if there is an error , i woul like to show data from database
// i tried to get data from snapshot like this : snapshot.data[0]...and snapshot.data[1]
// but no data is returned..
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.black,
),
);
} else {
if(snapshot.connectionState == ConnectionState.done){
return new Container(
color: Colors.black,
child: GridView.count(
padding: const EdgeInsets.all(20),
crossAxisCount: 2,
children: List.generate(snapshot.data.articles.length, (index) {
return new GestureDetector(
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsScreen(
image: snapshot.data.articles[index].urlToImage,
author: snapshot.data.articles[index].author,
title: snapshot.data.articles[index].title,
description: snapshot.data.articles[index].description,
publishedAt: snapshot.data.articles[index].publishedAt,
content: snapshot.data.articles[index].content,
))
);
},
child: Card(
elevation: 12,
child: new Column(
children: [
Image.network(snapshot.data.articles[index].urlToImage,
width: 250,),
Text(snapshot.data.articles[index].description)
],
),
),
);
}
)));
}
}
return new Center(
child: Visibility(
visible: true,
child: CircularProgressIndicator(
backgroundColor: Colors.black,
),
),
);
},
),
);
}

How to run the setState() after executing insert or delete of the moor in flutter

if you touch the heart icon, I want to insert or delete the product class in the Moor database and change the heart icon to setState()
Insertion or deletion is well executed, but the heart icon does not seem to change because setState() has already been executed during insertion or deletion.
I'd appreciate it if you could let me know if my method is wrong or if there's a better way than using a "stream builder."
Thank you for reading my question.
Widget setFavorite() {
ProductsDao productsDao = Provider.of<AppDatabase>(context).productsDao;
return StreamBuilder<List<mf.QueryRow>>(
stream: productsDao
.customSelect(
"SELECT * FROM Products WHERE firestoreid LIKE '${widget.product.firestoreid}'")
.watch(),
builder:
(BuildContext context, AsyncSnapshot<List<mf.QueryRow>> snapshot) {
if (snapshot.hasError) {
print(snapshot.error);
return new Text('Error: ${snapshot.error}');
}
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text("");
default:
return Positioned(
right: 0,
bottom: 0,
child: Padding(
padding: EdgeInsets.all(5),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
GestureDetector(
onTap: () {
setState(() {
snapshot.data.isEmpty
? productsDao.insertProduct(this.widget.product)
: productsDao.deleteProduct(this.widget.product);
});
},
child: Icon(
snapshot.data.isEmpty
? Icons.favorite_border
: Icons.favorite,
color: Colors.pink,
),
),
],
),
),
);
}
},
);
}
You can try to wait for the database to finish using await :
onTap: () async {
snapshot.data.isEmpty
? await productsDao.insertProduct(this.widget.product)
: await productsDao.deleteProduct(this.widget.product);
setState(() {}
});
},
I'm not sure how you have the method of inserting, but if you are using provider you need to use a function that will update a state and uses notifiyListeners().
When you are using notify listeners you don't even need setstate it will update all the values.
class AppDatabase extends ChangeNotifier {
ProductsDao _productsDao;
ProductsDao get productsDao => _productsDao;
void insertProduct(value){
this._productsDao.insertProduct(value);
notifiyListeners();
}
void deleteProduct(value){
this._productsDao.deleteProduct(value);
notifiyListeners();
}
}
Now you should use the provider insertProduct to refresh the state of your value.
Widget setFavorite() {
AppDatabase appState = Provider.of<AppDatabase>(context);
return StreamBuilder<List<mf.QueryRow>>(
stream: appState.productsDao
.customSelect(
"SELECT * FROM Products WHERE firestoreid LIKE '${widget.product.firestoreid}'")
.watch(),
builder:
(BuildContext context, AsyncSnapshot<List<mf.QueryRow>> snapshot) {
if (snapshot.hasError) {
print(snapshot.error);
return new Text('Error: ${snapshot.error}');
}
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text("");
default:
return Positioned(
right: 0,
bottom: 0,
child: Padding(
padding: EdgeInsets.all(5),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
GestureDetector(
onTap: () {
setState(() {
snapshot.data.isEmpty
? appState.insertProduct(this.widget.product)
: appState.deleteProduct(this.widget.product);
});
},
child: Icon(
snapshot.data.isEmpty
? Icons.favorite_border
: Icons.favorite,
color: Colors.pink,
),
),
],
),
),
);
}
},
);
This will refresh your value in the ui.
Hope it helped.

How can I access the innermost documents in nested collection structures?

As seen in the picture, there is a collection structure within the firestore. I want to show it with a listview by reaching the document information at the end. But I can't view it on the screen.
Code here:
#override
Widget build(BuildContext context) {
randevular = databaseRef
.collection(
'kuaforumDB/$_salonID/BekleyenRandevular/')
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: randevular,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if(!snapshot.hasData) {
return Column(
children:<Widget> [
SizedBox(
height: 100,
),
Center(
child: Image.asset("assets/images/icons/fon.webp",matchTextDirection: true,
height: 140.0,
width: 140.0,
),),
SizedBox(
height: 20
),
Center(
child: new Text('Henüz bir randevu oluşturmadınız.')
)
],
);
}
else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: new Center(
child: new CircularProgressIndicator(
value: null,
strokeWidth: 7.0,
),
)
);
} else {
return ListView(
children: snapshot.data.documents
.map((document) {
var query = databaseRef
.collection('kuaforumDB/')
.document('$_salonID')
.collection('BekleyenRandevular')
.document(document.documentID)
.collection('get')
.snapshots();
return StreamBuilder<QuerySnapshot> (
stream: query,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot2){
if (!snapshot2.hasData) return Text("Loading...");
return ListView(
children: snapshot2.data.documents
.map((DocumentSnapshot doc) => Card(
child: ListTile(
leading: IconButton(
tooltip: '',
icon: const Icon(Icons.check_circle, color: Colors.red,),
color: doc['randevuTarih']
.toDate()
.isBefore(DateTime.now())
? Colors.green
: Colors.orangeAccent,
iconSize: 30,
onPressed: () {},
),
title: Text(AppConstants.formatter
.format((doc['randevuTarih'].toDate())
.add(Duration(hours: 0)))
.toString()),
subtitle: Text('Randevu Onay Bekleniyor.'),
trailing: Icon(Icons.keyboard_arrow_right,
color: Colors.grey, size: 30.0),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (content) => MyPendingDetailPage(
salonID: _salonID.toString(),
userID: mPhone,
randevuID:
doc.documentID.toString(),
randevuTarih: AppConstants
.formatter
.format((doc['randevuTarih']
.toDate())
.add(Duration(hours: 0)))
.toString(),
randevuHizmet: doc['hizmetler'],
randevuFiyat:
doc['fiyat'].toString(),
randevuSure:
doc['sure'].toString(),
randevuFavori:
doc['favori'] == null
? false
: doc['favori'],
randevuBittimi:
doc['randevuTarih']
.toDate()
.isBefore(
DateTime.now())
? true
: false,
ayBasi: startofmonth,
sonrandevu : doc['randevuTarih'],
)));
}, )))
.toList(),
);
},
);
}).toList());
}
});
}
Using nested Listview in the code above may have caused a question. But I don't know how to solve this. When I check it, I see that I can actually pull the data, but I can't show it on the screen.

UnimplementedError error in FutureBuilder while displaying inserted data from database

I'm trying to create a Futurebuilder function to call and display all data that inserted in database unfortunately I got this error 'UnimplementedError' and im pretty stock on this any suggestion will be appreciated.
Here in my full code for implementation to display data in been trying to fix my error 'UnimplementedError' in which I'm trying to do is to display inserted in list view not in web view any suggestion will be appreciated.
body: Center(
child: Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FutureBuilder<ContactsDao>(
future: _calltheStream(),
builder: (BuildContext context,
AsyncSnapshot<ContactsDao> snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState == ConnectionState.none) {
return Container(
child: CircularProgressIndicator(),
);
} else {
return StreamBuilder<List<ContactObject>>(
stream: snapshot.data.findallContactsById(),
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState ==
ConnectionState.none) {
return Container(
child: CircularProgressIndicator(),
);
} else {
if(widget.Contactlist.length != snapshot.data.length){
widget.Contactlist = snapshot.data;
}
if(snapshot.data.length == 0){
return Center(
child: Text('No Data Found'),
);
}
return Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder:
(BuildContext context, int index) {
return Card(
child: ListTile(
leading: Checkbox(
value: widget.Contactlist[index].isSelect,
onChanged: (bool value) {
setState(() {
widget.Contactlist[index].isSelect = value;
});
},
),
trailing: GestureDetector(
onTap: () {
_selectedDetele(snapshot.data[index].id);
},
child: Icon(Icons.delete),
),
title: Text('${snapshot.data[index].task}',maxLines: 1,),
subtitle: Text('${snapshot.data[index].time}',style: TextStyle(fontSize: 10),),
));
}),
);
}
}); //DATA
} //DATA
}), // DATA
], // DATA
), // DATA
),//DATA
),
Future<ContactsDao> _calltheStream() async { //GET ALL DATA HERE
ContactDatabase contactDatabase = await widget.database;
widget._contactsdao = contactDatabase.contactsDao;
return contactDatabase.contactsDao;
}

How to call navigator inside Stream Builder in flutter?

I have a problem that when I try to put the the navigator pop() method inside the string builder this error appears: setState() or markNeedsBuild() called during build.
I read that you can't call the navigator inside the stream builder so does anyone have any ideia on how to fix this?
This is my code:
return Scaffold(
resizeToAvoidBottomPadding: false,
body: Center(
child: Container(
padding: EdgeInsets.all(15),
color: Colors.white,
child: Column(
children: <Widget>[
SizedBox(height: 55),
SvgPicture.asset('images/svg_example.svg'),
SizedBox(height: 55),
Text("Login App",
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.black)),
SizedBox(height: 40),
emailField,
SizedBox(height: 45),
passwordField,
SizedBox(height: 45),
loginButton,
SizedBox(height: 15),
StreamBuilder<ApiResponse<LoginResponse>>(
stream: userBloc.authenticationUserStream,
builder: (context,
AsyncSnapshot<ApiResponse<LoginResponse>> snapshot) {
// it will observe changes on the ApiResponse<LoginResponse>
if (!snapshot.hasData) return Container();
switch (snapshot.data.status) {
case Status.LOADING:
return Loading(
loadingMessage: "loading",
);
case Status.COMPLETED:
prefs.saveTokenPref(snapshot.data.data.token);
prefs.saveUserPref(snapshot.data.data.user);
goToMain();
return Container(width: 0.0, height: 0.0);
case Status.ERROR:
// Here you can go to another screen after login success.
return Center(
child: Text("${snapshot.data.message}"),
);
default:
return Container();
}
},
)
],
)),
),
);
}
goToMain() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainScreen()),
);
}
Ok, so given my question has a -1 because someone just feel like giving without really helping the post I put here the answer for this question:
You just need to do this on your initState()
This will listen to the stream itself and make UI logic outside the Stream Builder.
#override
void initState() {
userBloc = UserBloc();
super.initState();
userBloc.userSubject.listen((state) {
if (state.status == Status.COMPLETED) {
goToMain();
}
});
}