I have a ListView inside of a FutureBuilder. This FutureBuilder loads a list from disk and once loaded, shows a ListView. This ListView has a custom ItemTile to show for each loaded item. Each ItemTile has a button to copy itself (i.e. writing a copy of itself to memory). How can I now inform the ListView to be rebuild?
This is how my ItemTile is implemented (I only included the relevant code).
#override
Widget build(BuildContext context) {
return InkWell(
child: Card(
semanticContainer: false,
//This flag should be false if the card contains multiple different types of content.
child: Container(
height: 200,
width: double.infinity,
child:
Align(
alignment: Alignment.topRight,
child: getPopUpMenuButton(context),
)
),
),
);
}
PopupMenuButton<String> getPopUpMenuButton(BuildContext context) {
return PopupMenuButton(
onSelected: (str) {
switch (str) {
case 'MAKECOPY':
{
Item newItem = Item.clone(this.item, sameUID: false);
Scaffold.of(context).setState(() {
_itemManager.save(newItem);
});
}
}
},
icon: Icon(Icons.more_vert),
itemBuilder: (_) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
value: 'MAKECOPY',
child: Text(AppLocalizations.of(context).get('edit_as_copy'))),
],
);
}
I assumed the Scaffold.of(context).setState would do the trick but it does not. How do I correctly call a rebuild of the ListView/FutureBuilder from a list item?
Related
I have my custom Bottom Navigation Bar in one dart file, i.e. bottomnavbar.dart. And I have list of multiple screens(or pages) in my home.dart file. I am using an .obs variable to store my selected index value.
code from home.dart file:
var selectedIndex = 0.obs;
final screen = [
const Page1(),
const Page2(),
const Page3(),
const Page4(),
const Page5(),
];
...
body: screen[selectedIndex.value],
...
Even if I change the variable value (like 0.obs to 1.obs), page not changing, why??
next of, In my bottomnavbar.dart file, I have extracted and made a widget for my nav bar 'items'. And I have tried to wrap the item widget with Obx:
Widget bnbItems(String image, int index, double height) {
return Obx(
() => InkWell(
splashColor: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.5)
: Colors.pink.withOpacity(0.5),
enableFeedback: true,
onTap: () => setState(() {
selectedIndex.value = index;
_controller.animateTo(index / 4);
// print(selectedIndex);
}),
child: Container(
alignment: Alignment.center,
width: 50,
height: 50,
child: Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Image.asset(
image,
height: height,
),
),
),
),
);}
and I am getting this error:
[Get] the improper use of a GetX has been detected.
You should only use GetX or Obx for the specific widget that will be updated.
If you are seeing this error, you probably did not insert any observable variables into GetX/Obx
or insert them outside the scope that GetX considers suitable for an update
(example: GetX => HeavyWidget => variableObservable).
If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
Can anyone give me the solution with some code and explanation? And also how will I be able to set a particular screen as the initial screen?
Replace on tap with this code
onTap: () {
selectedIndex.value = 1; // page index you want to view
},
then remove Obx(()=> on bnbItems widget
Widget bnbItems(String image, int index, double height) {
return InkWell(
splashColor: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.5)
: Colors.pink.withOpacity(0.5),
enableFeedback: true,
onTap: () {
selectedIndex.value = 1; // page index you want to view
},
child: Container(
alignment: Alignment.center,
width: 50,
height: 50,
child: Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Image.asset(
image,
height: height,
),
),
),
);}
then use Obx(()=> wrapper on the body's widget
body: Obx(() => screen[selectedIndex.value]),
why you are using setState in GetX structure?
Try this code for onTap()
onTap: () {
selectedIndex.value = index;
_controller.animateTo(index / 4);
// print(selectedIndex);
},
to set initial screen use index no of that screen in var selectedIndex = 0.obs; instead of 0.
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
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();
}
)
I'm aiming for a page that looks like this -
ListView
[Profile _ Image] {Swiper}
[SizedBox]
[Profile Detail-1 ]{Text}
[Profile Detail-2 ]{Text}
[Profile Detail-3 ]{Text}
[Profile Detail-N ] {Text}
I looked at the Flutter cookbook example of MultiList
The cookbook expects all items in the listview to implement the same class. What if this is not possible.
I have tried using index of ListViewBuilder to return Widget based on index.
Is that the right approach? Shall I be doing something completely different - like siglechildScrollView?
Thanks!
Edit1-
Current Code that I'm using -
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
this._feedBloc.loadMore();
}
return false;
},
child: ListView.builder(
padding: EdgeInsets.only(bottom: 72),
itemCount: this._postItems.length + 1,
itemBuilder: (context, index) {
if (this._postItems.length == index) {
if (this._isLoadingMore) {
return Container(
margin: EdgeInsets.all(4.0),
height: 36,
width: 36,
child: Center(
child: CircularProgressIndicator(),
),
);
} else {
return Container();
}
}
if(index==0){
return WdgtProfileImage();}
else if(index==1){
return SizedBox(2.0);}
return WdgtUserPost(
model: this._postItems[index],
onPostClick: onPostClick,
);
//return postItemWidget(
// postItem: this._postItems[index], onClick: onPostClick);
}),
);
You can use a CustomScrollView instead of the normal Listview.builder. The CustomScrollView takes in a list of slivers to which you can pass/use a SliverList to build a list.
CustomScrollView(
slivers: <Widget>[
//A sliver widget that renders a normal box widget
SliverToBoxAdapter(
child: WdgtProfileImage(),
),
//A sliver list
SliverList(
//With SliverChildBuilderDelegate the items are constructed lazily
//just like in Listview.builder
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return WdgtUserPost(
model: _postItems[index],
onPostClick: onPostClick,
);
},
childCount: _postItems.length,
),
),
if (_isLoadingMore)
//your loading widget shown at the bootom of the list
SliverToBoxAdapter(
child: Container(
margin: EdgeInsets.all(4.0),
height: 36,
width: 36,
child: Center(
child: CircularProgressIndicator(),
),
),
),
],
)
Additional links to docs:
SliverList
SliverChildBuilderDelegate
SliverToBoxAdapter
This is a mockup of what I want to achieve - it is a CupertinoActivityIndicator underneath a CupertinoSliverNavigationBar for notifying the user that the data is being downloaded.
Once it is downloaded, it should look like so:
Now, I have been trying to get this effect using the following code:
List<Trade> trades = [];
showLoadingDialog() {
return trades.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getTradeItemList();
}
}
getTradeItemList() {
return new CupertinoPageScaffold(
child: new CustomScrollView(slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: const Text('Coffee Shop'),
),
getBody(),
]));
}
getProgressDialog() {
return new Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
),
child: new Center(child: const CupertinoActivityIndicator()));
}
However, I'm receiving this error because I'm trying to put a non-RenderSliver type into a Sliver. In other words, I'm putting in an CupertinoActivityIndicator into a Sliver and the code is rejecting it.
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY
╞═══════════════════════════════════════════════════════════ The
following assertion was thrown building Container(bg:
BoxDecoration(color: Color(0xffffffff))): A RenderViewport expected a
child of type RenderSliver but received a child of type
RenderDecoratedBox. RenderObjects expect specific types of children
because they coordinate with their children during layout and paint.
For example, a RenderSliver cannot be the child of a RenderBox because
a RenderSliver does not understand the RenderBox layout protocol.
The closest I could get to achieving the effect I want is displayed in the gif below. However, as you can clearly see, the CupertinoSliverNavigationBar is not being displayed when the CupertinoActivityIndicator is visible. It is only after the data has been downloaded that the CupertinoSliverNavigationBar that says "Coffee Shop" is visible.
I achieved the above using the following code:
List<Trade> trades = [];
showLoadingDialog() {
return trades.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getTradeItemList();
}
}
getProgressDialog() {
return new Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
),
child: new Center(child: const CupertinoActivityIndicator()));
}
getTradeItemList() {
return new CupertinoPageScaffold(
child: new CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: const Text('Coffee Shop'),
),
new SliverPadding(
// Top media padding consumed by CupertinoSliverNavigationBar.
// Left/Right media padding consumed by Tab1RowItem.
padding: MediaQuery
.of(context)
.removePadding(
removeTop: true,
removeLeft: true,
removeRight: true,
)
.padding,
sliver: new SliverList(
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Tab1RowItem(
index: index,
lastItem: index == trades.length - 1,
color: "Coffee Beans",
colorName: "Buy coffee now",
);
},
childCount: trades.length,
),
),
)
],
),
);
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return getBody();
}
#override
void initState() {
super.initState();
loadDataTrades();
}
Can anyone tell me how I can achieve the effect I want?
Widget _mainFrame;
#override
Widget build(BuildContext context) {
return new CustomScrollView(
slivers: <Widget>[
new CupertinoSliverNavigationBar(
largeTitle: const Text("Coffe Shop"),
),
_mainFrame,
],
);
}
Widget _beforeDataLoaded() {
return new SliverFillRemaining(
child: new Container(
child: new Center(
child: new CupertinoActivityIndicator(),
),
),
);
}
Widget _dataLoadComplete() {
return new SliverList(
// Your list items
);
}
You change the _mainFrame Widget from the CupertionActivityIndicator to your listview if the load finished.