How to reload specific widgets in flutter without its parent? - flutter

I am building an app in Flutter,that supposed to be a social network app-similar to faceboook.
I have implemented a like button-when pressed is sending the server the request,and then depending on the status code it sets the state.My problem begins when the setState() is rendering again the avatar picture,or creating it again from scratch(the avatar is stored in a 64base String).
the likePress is a future that sends the request and then sets the boolean isLiked accordingly.
this is the creating of the like button:
buildLikeButton(int ownerId, int postId) {
return RepaintBoundary(
child: FutureBuilder<bool>(
future: getLike(ownerId, postId),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
IconButton likeButton;
if (snapshot.hasData) {
isLiked = snapshot.data;
likeButton = createLikeButton(ownerId, postId);
} else if (snapshot.hasError) {
isLiked = false;
likeButton = createLikeButton(ownerId, postId);
print('the snapshot has an error ${snapshot.error}');
} else {
isLiked = false;
likeButton = createLikeButton(ownerId, postId);
}
return likeButton;
}));
}
createLikeButton(int ownerId, int postId) {
return IconButton(
icon: returnLikeIcon(isLiked),
color: Theme.of(context).accentColor,
onPressed: () async {
if (this.mounted) {
setState(() {
Future lol = likePress(ownerId, postId).then((onValue) {});
});
}
},
);
}
and this is the creation of the avatar:
createAvatar(BuildContext context, avatar_base64, int ownerId) {
Uint8List bytes = base64Decode(avatar_base64.split(',').last);
return RepaintBoundary(
child: CircleAvatar(
radius: 25.0,
backgroundImage: MemoryImage(bytes),
backgroundColor: Colors.transparent,
));
}
The widget that displays them together is the Post widget which i have created for this project,and this is it's build function:
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(0.2),
child: Container(
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor,
blurRadius: 1.0,
spreadRadius: 1.0, // has the effect of extending the shadow
offset: Offset(
5.0, // horizontal, move right 10
5.0, // vertical, move down 10
),
),
]),
child: Card(
elevation: 10.0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
fit: FlexFit.loose,
child: postInfo(context, time, ownerId)),
Divider(
thickness: 1.0,
height: 10.0,
indent: 10.0,
endIndent: 10.0,
),
postContent(content),
Divider(
thickness: 1.0,
height: 10.0,
indent: 10.0,
endIndent: 10.0,
),
createButtonBar(ownerId, postId),
],
)),
));
}
postInfo is just a FutureBuilder that builds the ListTile that adds up the avatar and the name, and createButtonBar creates the like button and 2 more buttons.
I would like to change the icon when the user presses the like button,but only if the server has responded with the right status code and without rendering and creating the whole Post widget over again.Thank you for the trouble!

This means that the avatar is beneath the point where you are calling setState(() {}). In your case, the method is probably inside that particular widget and the widget is being rebuilt.
I suggest for you to solve the problem to move the creation of the avatar above. In this way, if you need to rebuild the object the avatar will not be created anew but simply placed within the new widget. Place some debugPrints around to speed up the process and try to refactor the code to see if you missed something.

After taking a better look at my code,I decided to create a different Widget for each part of the post,so I can initialize everything that will not be built again outside of the build method.
so if you want to exclude a widget from the setState() method, you need to move it outside the current widget(by creating a widget for it) and just create an instance of it as a parameter in the constructor.
In more detail,I created a class named PostHeader and there i created the avatar and the ListTile containing it,then i created an instance of it inside the Post class,so it is not created inside the build method of the Post class.

Related

How to remove item at the top of List View in Flutter?

I have List View and I have inside each item in the list a button called "Delete item". When I press that button inside each item I want to delete only that item from the list.
But it does not delete item, it just display Toast message that I have specified.
How I can solve this?
This is the code:
Widget build(BuildContext context) {
listItems = buildVCsFromAPI(context);
return Container(
child: ListView.builder(
itemBuilder: (context, index) =>
_buildListItem(context, listItems[index], index),
itemCount: listItems.length,
physics: AlwaysScrollableScrollPhysics()),
);
}
Widget _buildListItem(
BuildContext context, _VerifiableCredentialListItem cert, int index) {
return GestureDetector(
child: AnimatedAlign(
curve: Curves.ease,
duration: Duration(milliseconds: 500),
heightFactor: selectedPosition == index ? factorMax : factorMin,
alignment: Alignment.topCenter,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)), //here
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
offset: Offset(0, -1),
blurRadius: 10.0)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HeadingRow(title: cert.fullTitle, appIcon: cert.appIcon),
displayListItem(index, selectedPosition, cert)
],
),
),
),
}
Column displayListItem(
int index, int selectedIndex, _VerifiableCredentialListItem cert) {
CredentialListGroupType groupType = cert.groupType;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: UIConstants.s2,
),
buildAnotherWidget(),
SizedBox(
height: UIConstants.s3,
),
buildDeleteAndExportButtons(),
],
);
}
Column buildDeleteAndExportButtons() {
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(
vertical: UIConstants.s1, horizontal: UIConstants.s2),
child: Row(
children: [
Expanded(
flex: 1,
child: BlueButtonWithIcon(
text: 'Delete item',
icon: 'assets/icons/delete-icon.svg',
onPressed: () {
setState(() {
AppToaster.pop(ToasterType.info, "Delete");
listItems.removeAt(0);
});
},
),
),
SizedBox(width: UIConstants.s1),
Expanded(
flex: 1,
child: BlueButtonWithIcon(
text: 'Export',
icon: 'assets/icons/export.svg',
onPressed: null,
),
)
],
),
),
SizedBox(height: UIConstants.s1)
],
);
}
Calling setState doesn't mean that flutter would actually full repaint the screen it means that it will check your widget tree with the last rendered widget tree and it will paint only the differences and it first compares widgets type and then widget keys to find that there is a difference between the current widget and the previous one and because of this when you remove an item from your list of items flutter checks your returned widgets to the currently rendered widget it doesn't found any difference and it won't repaint the screen and continues showing the last render
So for you to tell the flutter that one of the items in the listView is changed you could assign a uniqueKey key for each list item widget note that for this topic your keys should be unique to the data of that widget otherwise you will face performance issues because if your widget key is changed without any change in the representation of that widget in next time that builds method is called which could happen frequently flutter compares widgets key with the previous widgets key which is rendered to the screen and exist on the render tree and it founds that the keys are different and it repaints that widget which is a redundant operation because your widgets UI and representation are the same
For example, assign a unique id base on the index or content of your data to each data model in the listItems and use that to create a ValueKey() for the widget that is represented by that data
here is a working example of the list which when you click on the list item first list item will be removed
class ListItemDataModel {
final String id;
final Color color;
ListItemDataModel(this.id, this.color);
}
class _MyHomePageState extends State<MyHomePage> {
List<ListItemDataModel> items = [];
#override
void initState() {
super.initState();
items = [
ListItemDataModel("A", Colors.red),
ListItemDataModel("B", Colors.amber),
ListItemDataModel("C", Colors.green),
ListItemDataModel("D", Colors.lightBlueAccent),
ListItemDataModel("E", Colors.pink),
];
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
key: ValueKey(items[index].id),
//Tap to Remove first item from list
onTap: () {
items.removeAt(0);
setState(() {});
},
child: Container(
height: 60,
color: items[index].color,
child: Center(
child: Text(
"This is a unique item with id = ${items[index].id}"),
),
),
);
},
itemCount: items.length,
),
),
);
}
}
So,
We don't have acces to the code above.. so.. where does listItems came from?
Maybe you are retrieving the value of listItems after the init state? if so it's normal that you are retrieving always the same result..
What you should do is the following:
get listItems value from params, global vars, databse ecc
display the list
when you delete a single item you should update the original list
on state updated now the list will be loaded with updated values
If you delete an item from a list but the list is then reloaded in its original form your updates will be lost

Flutter async method keeps running even after the corresponding widget is removed

I have a list of image paths and I am using the List.generate method to display images and a cross icon to remove image from list. Upload method is called on each image and when I remove the image from the list the method still keeps running until the image is uploaded. The behavior I am expecting is when I remove the image from the list the method should also stop running. I am using a future builder to display the circular progress bar and error icons while uploading an image.
What should I be doing to make sure the future method associated to the current widget also stops when I remove the widget from the list?
This is the code where I am creating a list
List.generate(
files.length,
(index) {
var image = files[index];
return Container(
height: itemSize,
width: itemSize,
child: Stack(
children: <Widget>[
Container(
getImagePreview(image, itemSize)
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
uploadHandler(image, field),
InkWell(
onTap: () => removeFileAtIndex(index, field),
child: Container(
margin: EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(.7),
shape: BoxShape.circle,
),
alignment: Alignment.center,
height: 22,
width: 22,
child: Icon(Icons.close, size: 18, color: Colors.white),
),
),
],
),
],
),
);
},
)
This is Upload Handler method.
Widget uploadHandler(file, field) {
return FutureBuilder(
future: upload(file),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data.statusCode == 201) {
return doneUpload();
} else {
logger.d(snapshot.error);
return error();
}
} else {
return uploading();
}
},
);
}
The lifecycle of the widget isn't attached to the async functions invoked by the widget.
You can check the mounted variable to check if the widget still mounted from your async function.

How to update a Widget inside a List

I have a list of chats, and I want to show on each chat card, if there's a new message that the user hasn't read.
The list is in a StatefulWidget, the list contains refactored cards that are also StatefulWidgets, I also made the code to work with Firestore to check if the user has read the message, but still I don't know what's happening, because it doesn't update the icon of unread messages.
The data changes in the database, but it doesn't in the chat card. If I reload the app, because the cards are rebuilt, then it does change.
Here's the chat card code:
bool hasUnreadMessages = false;
void unreadMessagesVerifier() {
setState(() {
_firestore.collection('chatRoom').document(_chatRoomID).get().then((data) async {
hasUnreadMessages = await data['hasUnreadMessages'];
});
});
}
#override
Widget build(BuildContext context) {
unreadMessagesVerifier();
return GestureDetector(
child: Stack(
children: <Widget>[
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Container(
width: double.infinity,
child: Text(
widget.lastMessage,
),
),
),
hasUnreadMessages
? Container(
margin: EdgeInsets.fromLTRB(10, 0, 5, 0),
child: CircleAvatar(
radius: 7,
backgroundColor: Colors.blue,
),
)
: SizedBox(),
],
),
),
],
),
onTap: widget.onTap,
); // ChatCard
}
If more info is needed, do let me know!
========================================================================
EDIT:
Fixed thanks to #Pedro R.
I just had to move the SetState() and check the mounted
void unreadMessagesVerifier() {
_firestore.collection('chatRoom').document(_chatRoomID).get().then((data) async {
if (mounted) {
setState(() {
hasUnreadMessages = data['hasUnreadMessages'];
});
}
});
}
I think your problem lies in the way you are calling setState.
Try calling it after the future finishes.
Like this:
void unreadMessagesVerifier() {
_firestore.collection('chatRoom').document(_chatRoomID).get().then((data) =>
data['hasUnreadMessages'].then(result){
setState((){
hasUnreadMessages = result;
});
});
}
Sorry for the formatting by the way.
Consider using StatefulBuilder class, it rebuilds the particular Widget which it wraps based upon the value getting updated
So, hasUnreadMessage will be used to update the Container(). Do something like this
StatefulBuilder(
builder: (BuildContext context, StateSetter setState){
// here you return the data based upon your bool value
return hasUnreadMessages ? Container(
margin: EdgeInsets.fromLTRB(10, 0, 5, 0),
child: CircleAvatar(
radius: 7,
backgroundColor: Colors.blue,
)
) : SizedBox();
}
)

Flutter ListView doesnt update when the underlying list is updated

I am building this card game app which has a list of Cards (containers with special effects) and the list is managed and updated by the Provider/Consumer mechanism.
It looks like this
class LowerSectionWithScrollingCardList extends StatefulWidget {
#override
_LowerSectionWithScrollingCardListState createState() =>
_LowerSectionWithScrollingCardListState();
}
class _LowerSectionWithScrollingCardListState
extends State<LowerSectionWithScrollingCardList> {
#override
Widget build(BuildContext context) {
return Consumer<GameState>(builder: (context, gameState, child) {
print('lower list ${gameState.gcurrentPlayers[0].ownList}');
return Expanded(
flex: 34,
child: Container(
color: Colors.white,
child: ListView(
children: gameState.gcurrentPlayers[0].ownList,
scrollDirection: Axis.horizontal,
),
),
);
});
}
}
gameState.gcurrentPlayers[0].ownList is the first player which is us, and ownlist is the actual list of widgets or cards which gets updated by clicking some other buttons in the app.
the list is updated by this method exactly
void ggiveCardToCurrentPlayer(int howMuch){
for(int i=0;i<howMuch;i++)
ggetPlayerWithCurrentTurn().ownList.add(gplayingCards.removeLast());
notifyListeners();
}
Now after the "notifylisteners" is called, I am 100% sure that the Consumer is updated with the new data, because the print statement in the build method prints the newly added cards.
Finally, the issue is that the listView itself doesn't update while the list it renders has those added cards.
I checked out a few posts regarding a similar issue and they suggest that one add keys to the Data items, in my case the data items are my cards, and I added keys to them. No change.
class RegularUnoCard extends StatelessWidget{
final Color _color;
final String _value;
final Key _key;
RegularUnoCard(this._color, this._value,this._key);
#override
Widget build(BuildContext context) {
return Container(
key: _key,
margin: EdgeInsets.symmetric(
vertical: _cardMarginVer, horizontal: _cardMarginHor),
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_cardCornerRadii),
border: Border.all(color: _color, width: 4, style: BorderStyle.solid),
boxShadow: [
BoxShadow(
color: _color,
spreadRadius: (_value == plus2) ? 8 : 2,
blurRadius: 5)
],
color: Colors.white,
),
child: Container(
height: _cardHeight,
width: _cardWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(60),
color: _color,
),
child: Center(
child: getLogo(),
),
),
);
}
I hope this the correct way of putting keys in the Cards.
I also read that one must call setState() but I dont have any place to call Setstate from within my listView.
I have tried replacing the ownList logic with Provider.of(context).playerlist[0].ownlist etc etc but that too doesnt work
I hope I have supplied enough data for this evaluation. Please comment if more information is required. Thanks a lot for your time and suggestions.
I read more about the problem, the source which was helpful was this
Basically my list was being updated but I was providing the reference of the array, and since flutter works on immutable data, it did not detect my array change. So all I had to do was to build a new list from the existing reference array.
children: List<Widget>.from(gameState.gcurrentPlayers[0].ownList),
The final ListView should look like
#override
Widget build(BuildContext context) {
return Consumer<GameState>(builder: (context, gameState, child) {
print('lower list ${gameState.gcurrentPlayers[0].ownList}');
return Expanded(
flex: 34,
child: Container(
color: Colors.white,
child:ListView(
children: List<Widget>.from(gameState.gcurrentPlayers[0].ownList),
scrollDirection: Axis.horizontal,
),
),
);
},key: UniqueKey(),);
}
}
Now my card game is updating with the new cards!!

Maintaining state in Listview.Builder() of Stateful widget (a progress bar) problem

I am creating a listview using listview.builder. Each view is a card with a button on it and a progress bar (See screenshot)
Each card is a stateful widget. When you press the button, the progressbar starts a 5 second timer. When the timer is over, the object is removed from the LinkedHashMap which makes is dissapear from the screen too. This works, if only one button is pressed in the displayed list. If one button is pressed and after 2 seconds second or/and third button(s) are pressed, the animation of progress bar can be seen on all three, but as soon as a first one dissapears from the screen and the second and subsequest are re-drawn, they loose there state and they appear as if their buttons were never pressed.
What I am trying to do is, keep their states independent and keep their animations independent. When one card dissapears, the others gets shifted up/down accordingly but the progress animation to continue as it is as currently they seem to redraw themselve in unpressed/original state.
My code for my card is as follows:
class _DeliveryCardsViewState extends State<DeliveryCardsView> {
double percentage = 1.0;
bool countdownStarted = false;
String buttonText = "CLAIM ORDER";
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
child: Container(
padding: EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10.0, right: 10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text('#'+widget.delivery.uID),
GestureDetector(
onTap: () async {
if (!countdownStarted) {
countdownStarted = true;
setState(() {
buttonText = 'UNDO';
});
int j = 100;
while (j > 0) {
await Future.delayed(Duration(milliseconds: 50)); // Duration in seconds (50/10) = 5 seconds
j -= 1;
setState(() {
if (countdownStarted){
percentage = (j / 100).toDouble();
} else {
percentage = 1.0;
}
});
if (!countdownStarted){
setState(() {
buttonText = 'CLAIM ORDER';
});
break;
}
}
//Try to claim it now
if (countdownStarted) tryClaimOnFirebase();
} else {
countdownStarted = false;
}
},
child: Container(
padding: EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
Text(buttonText,style: TextStyle(color: Colors.white),),
SizedBox(height: 5.0,),
LinearPercentIndicator(
width: 100.0,
lineHeight: 7.0,
percent: percentage,
backgroundColor: Colors.green,
progressColor: Colors.red,
),
],
),
decoration: BoxDecoration(
color: mainGrey,
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
)
),
],
),
],
),
),
);
}
And my Listview.builder code is as follows:
return Container(color: Colors.grey[350],
child: ListView.builder(
key: UniqueKey(),
itemCount: deliveriesAvailable.length,
controller: _controller,
scrollDirection: Axis.vertical,
reverse: false,
shrinkWrap: true,
itemBuilder: (context,position){
return GestureDetector(
child: DeliveryCardsView(delivery: deliveriesAvailable.values.elementAt(position),),
onTap: (){},
);
}),
);
Error recording:
Working now.
Alright so, I think I underdtand what was happening. The entire stateful widget was getting disposed and rebuilt and as a result the 'logic' 'inside' the stateful widget was also resetting. What I did was the following:
1) Added a couple of extra variables in the 'Object'
2) When the view gets built again, it checks those variables and then 'resumes' the state it was last disposed in, and that included the the value of the progress bar counter.
Obvously for this to happen, the entire logic was taken out from onTap() and added to a new async method.
Simplessss :)