I have a bottom nav bar that uses this code to switch widgets:
void _onItemTapped(int index) {
setState(() {
selectedIndexGlobal = index;
print(index);
});
}
And shows the needed widgets in body:
body: SafeArea(
child: new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
Container con = Container(
child: _widgetOptions.elementAt(selectedIndexGlobal)
);
return con;
}
))
I also have a GridView with static children list loaded from Firebase, and tried to apply a GestureDetector to it, to change selectedIndex and update screen state:
static getExpenseItems(AsyncSnapshot<QuerySnapshot> snapshot, BuildContext context) {
try {
snapshot.data.documents.sort((a, b) =>
a.data["order"].compareTo(b.data["order"]));
} on NoSuchMethodError {}
return snapshot.data.documents
.map((doc) =>
new GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
catName = doc["catName"];
setState(() {
selectedIndexGlobal = 4;
});
},
child: new Container(
child: Container(
padding: EdgeInsets.only(top: 15, left: 10, bottom: 15),
decoration: new BoxDecoration(
border: new Border.all(color: Colors.grey[200],
width: 0.8)),
child: Align(
alignment: Alignment.centerLeft,
child: Text(doc["name"],
style: TextStyle(fontSize: categoriesFont),
textAlign: TextAlign.center),
)
)
)
)
).toList();
}
But setState can't be called from a static method, as well as GridView.count doesn't let me use non-static widgets as child. What should I do then to update state on click?
This line will help you get the instance of the Widget State object you need.
All you need access to is the current context object:
_HomeState stateObject = context.findAncestorStateOfType<_HomeState>();
stateObject.setState(() {
_tabIndex = index;
});
For this to work, ensure that the context you use to access the State Object is from a child widget of the State object you are trying to find.
Related
Im trying to show a listView.builder inside a AlertDialog, and Im filling the its list by calling a function everytime the button to open the AlertDialog is pressed but the problem is that the ui doesn’t update when the list is filled with the data, I'm using getX and I'm very new to it, can someone show me what I'm doing wrong?
I'm using the GetX builder:
GetX<DashboardController>(
init: Get.put<DashboardController>(DashboardController()),
builder: (DashboardController dashboardController) {
return GridView.builder(
My Get.dialog function:
return GestureDetector(
onTap: () {
// this is where I'm filling the list
dashboardController
.callEmployeeCheckInOutList(_employeeModel.id);
Get.dialog(
AlertDialog(
contentPadding: EdgeInsets.zero,
content: SizedBox(
height: size.height * 0.55,
width: size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
EmployeeProfileWidget(
size: size,
profileBackgroudPath: profileBackgroudPath,
employeeModel: _employeeModel,
),
// this is where my listview.builder resides
EmployeeActivityWidget(
closeCrossPath: closeCrossPath,
employeeCheckInOutList:
_employeeCheckInOutList,
employeeModel: _employeeModel,
onTap: () {},
),
],
),
),
),
);
},
My listview.builder:
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32.0,
right: 50.0,
),
child: ListView.builder(
itemCount: employeeCheckInOutList.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final _checkInOutModel = employeeCheckInOutList[index];
return SizedBox(
height: 120,
child: TimelineTile(
beforeLineStyle: const LineStyle(
color: Color(0xffa5affb),
),
My Controller:
Rx<List<CheckInOutModel>> _employeeCheckInOutList =
Rx<List<CheckInOutModel>>([]);
List<CheckInOutModel> get employeeCheckInOutList =>
_employeeCheckInOutList.value;
Future<void> callEmployeeCheckInOutList(String id) async {
_employeeCheckInOutList =
await EmployeeService.employeeCheckInOutFuture(docId: id);
update();
}
Use .assignAll method on the RxList to trigger UI update:
Future<void> callEmployeeCheckInOutList(String id) async {
final result = await EmployeeService.employeeCheckInOutFuture(docId: id);
_employeeCheckInOutList.assignAll(result);
}
And you don't need to call update() when using Rx.
I already faced same issue.
Solution:
Simply use again GetX<Controller> inside AlertDialog
like
GetX<DashboardController>(
init: Get.put<DashboardController>(DashboardController()),
builder: (DashboardController dashboardController) {
return GridView.builder(
.....
Get.dialog(
AlertDialog(
contentPadding: EdgeInsets.zero,
content: GetX<DashboardController>(
init: Get.put<DashboardController>(DashboardController()),
builder: (DashboardController dashboardController) {
SizedBox(
One expansion panel was expanded, and when the other expansion panel was expanded, the previous expanded one was set to be closed.
For example, when panel 1 is expanded and panel 2 is clicked, panel 1 is closed and panel 2 is expanded.
By the way, I tried to close panel 1 after expanding panel 1, but it didn't close.
How should I solve this?
class _RecordState extends State<Record> {
#override
void initState() {
super.initState();
}
int? _activeMeterIndex;
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: new ListView.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int i) {
return Card(
margin:
const EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 0.0),
child: new ExpansionPanelList(
expansionCallback: (int index, bool status) {
setState(() {
_activeMeterIndex = (_activeMeterIndex == i ? _activeMeterIndex : i);
});
},
children: [
new ExpansionPanel(
isExpanded: _activeMeterIndex == i,
headerBuilder: (BuildContext context,
bool isExpanded) =>
new Container(
padding:
const EdgeInsets.only(left: 15.0),
alignment: Alignment.centerLeft,
child: new Text(
'list-$i',
)),
body: new Container(child: new Text('content-$i'),),
),
],
),
);
}),
);
}
you got the issue for the isExpanded: _activeMeterIndex == i, you must be used a unique index for each expansion tile and The callback gets called whenever one of the expand/collapse buttons is pressed. The arguments passed to the callback are the index of the pressed panel and whether the panel is currently expanded or not.
List<bool> _isExpand = [false, false,];
new ExpansionPanel(
isExpanded: _isExpand[i],
headerBuilder: (BuildContext context,
bool isExpanded) =>
new Container(
padding:
const EdgeInsets.only(left: 15.0),
alignment: Alignment.centerLeft,
child: new Text(
'list-$i',
)),
body: new Container(child: new Text('content-$i'),),
),
// callback
expansionCallback: (int index, bool status) {
setState(() {
_isExpand[i] = !status;
});
},
I was trying to get the list page refreshed if a method was run on another page. I do pass the context using the push navigation.
I tried to follow these 3 answers Answer 1 Answer 2 and Answer 3 and I am not able to manage the states here.
This is the first list page which needs to be refreshed. It calls a class
class _PageLocalState extends State<PageLocal> {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: SafeArea(
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: widget.allLocal.length,
//padding: const EdgeInsets.only(top: 10.0),
itemBuilder: (context, index) {
return LocalCard(widget.allLocal[index]);
},
)),
)
],
),
);
}
}
The next class:
class LocalCardState extends State<LocalCard> {
FavData localdet;
LocalCardState(this.localdet);
ListTile makeListTile() => ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
title: Text(
localdet.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(localdet.loc),
trailing: Icon(Icons.keyboard_arrow_right, size: 30.0),
onTap: () => navigateToDetail(localdet),
);
Widget get localCard {
return new Card(
elevation: 4.0,
margin: new EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
child: Container(
child: makeListTile(),
));
}
#override
Widget build(BuildContext context) {
return new Container(
child: localCard,
);
}
navigateToDetail(FavData localdet) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FavouriteDetailPage(
mndet: localdet,
)));
setState(() {});
}
}
Now this is routing to the final detail page:
class _FavouriteDetailPageState extends State<FavouriteDetailPage> {
bool isFav = false;
FavData mndet;
_FavouriteDetailPageState(this.mndet);
// reference to our single class that manages the database
final dbHelper = DatabaseHelper.instance;
#override
Widget build(BuildContext context) {
Widget heading = new Container(...);
Widget middleSection = new Expanded(...);
Widget bottomBanner = new Container(...);
Widget body = new Column(...);
final makeBottom = Container(
height: 55.0,
child: BottomAppBar(
color: Color.fromRGBO(36, 36, 36, 1.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new FavIconWidget(mndet),
],
),
),
);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('The Details'),
backgroundColor: Color.fromRGBO(36, 36, 36, 1.0),
),
body: Container(
child: Card(
elevation: 5.0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.white70, width: 1),
borderRadius: BorderRadius.circular(10),
),
margin: EdgeInsets.all(20.0),
child: Padding(
padding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
child: body,
),
),
),
bottomNavigationBar: makeBottom,
);
}
void share(BuildContext context, FavData mndet) {
final RenderBox box = context.findRenderObject();
final String shareText = "${mndet.name} - ${mndet.desc}";
Share.share(shareText,
subject: mndet.loc,
sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size);
}
}
class FavIconWidget extends StatefulWidget {
final FavData mnforIcon;
FavIconWidget(this.mnforIcon);
#override
_FavIconWidgetState createState() => _FavIconWidgetState();
}
class _FavIconWidgetState extends State<FavIconWidget> {
final dbHelper = DatabaseHelper.instance;
Future<bool> get isFav async {
final rowsPresent = await dbHelper.queryForFav(widget.mnforIcon.id);
if (rowsPresent > 0) {
print('Card Loaded - Its Favourite already');
return false;
} else {
print('Card Loaded - It is not favourite yet');
return true;
}
}
void _insert() async {...}
void _delete() async {...}
#override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: isFav,
initialData:
false, // you can define an initial value while the db returns the real value
builder: (context, snapshot) {
if (snapshot.hasError)
return const Icon(Icons.error,
color: Colors.red); //just in case the db return an error
if (snapshot.hasData)
return IconButton(
icon: snapshot.data
? const Icon(Icons.favorite_border, color: Colors.white)
: Icon(Icons.favorite, color: Colors.red),
onPressed: () => setState(() {
if (!snapshot.data) {
print('Its favourite so deleting it.');
_delete();
} else {
print('Wasnt fav in the first place so inserting.');
_insert();
}
}));
return CircularProgressIndicator(); //if there is no initial value and the future is not yet complete
});
}
}
I am sure this is just some silly coding I have done but just not able to find out. Where.
I tried adding Navigator.pop(context); in different sections of the detail page and it fails.
Currently, I have to navigate back to the Favourites list page and then HomePage and then back to Favourites ListPage to refresh the list.
try this.. Anywhere you are using Navigator.pop or Navigator.push .. Instead of this use this:
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => Password())
);
//instead of Password use the name of the page(the second page you want to go to)
i am developin app i just got some of those code from an app i just need to cart page button make a floating action button this button shows how many products in cart also i want to do this if counter=0 i need to hide that FloatingActionButton if they added item to basket just show that time if you have any suggestions thanks a lot for now
'class CustomAppBar extends StatelessWidget {
#override
Widget build(BuildContext context) {
final CartListBloc bloc = BlocProvider.getBloc<CartListBloc>();
// TODO: implement build
return Container(
margin: EdgeInsets.only(bottom: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
StreamBuilder(
stream: bloc.listStream,
builder: (context, snapshot) {
List<FoodItem> foodItems = snapshot.data;
int length = foodItems != null ? foodItems.length : 0;
return buildGestureDetector(length, context, foodItems);
},
)
],
),
);
}
GestureDetector buildGestureDetector(
int length, BuildContext context, List<FoodItem> foodItems) {
return GestureDetector(
onTap: () {
if (length > 0) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Cart()));
} else {
return;
}
},
child: Container(
margin: EdgeInsets.only(left: 30),
child: Text(length.toString()),
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.yellow[800], borderRadius: BorderRadius.circular(50)),
),
);
}'
First, you need a stateful widget and not a stateless one. If you are using VS code, it can actually do it for you automatically.
Once you have that, one variable will be "length".
String length;
void initState() {
length=0;
}
Whenever you increment it up, you have to call...
setState(() {
length+=1;
});
or down...
setState(() {
length-=1;
});
your floating action button will be...
(length==0)?
Container():
GestureDetector(
onTap: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Cart()));
},
child: Container(
margin: EdgeInsets.only(left: 30),
child: Text(length.toString()),
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.yellow[800], borderRadius: BorderRadius.circular(50)),
),
);
This means that if the cart length is 0, it will show an empty container.
If the cart is not 0, it will show a button with length
It is kind of a complex problem but I'll do my best to explain it.
My project utilizes a sqflite database. This particular page returns a list of Dismissible widgets according to the data in the database. This is how I read the data:
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
Future<List<Task>> allTasks;
#override
void initState() {
allTasks = dbProvider.getAllTasks();
super.initState();
}
void update(){
setState(() {
allTasks = dbProvider.getAllTasks();
});
}
//build
}
The TaskList widget returns a page with a FutureBuilder, which builds a ListView.builder with the data from the database. The ListView builds Dismissible widgets. Dismissing the Dismissible widgets updates a row in the database and reads the data again to update the list.
build method for TaskListState
#override
Widget build(BuildContext context) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets such as a title for the list
),
FutureBuilder(
future: allTasks,
builder: (context, snapshot){
if(snapshot.hasError){
return Text("Data has error.");
} else if (!snapshot.hasData){
return Center(
child: CircularProgressIndicator(),
);
} else {
return pendingList(Task.filterByDone(false, Task.filterByDate(Datetime.now, snapshot.data))); //filters the data to match current day
}
},
),
//other widgets
],
);
}
The pendingList
Widget pendingList(List<Task> tasks){
//some code to return a Text widget if "tasks" is empty
return ListView.separated(
separatorBuilder: (context, index){
return Divider(height: 2.0);
},
itemCount: tasks.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index){
return Dismissible(
//dismissible backgrounds, other non-required parameters
key: Key(UniqueKey().toString()),
onDismissed: (direction) async {
Task toRemove = tasks[index]; //save the dismissed task for the upcoming operations
int removeIndex = tasks.indexWhere((task) => task.id == toRemove.id);
tasks.removeAt(removeIndex); //remove the dismissed task
if(direction == DismissDirection.endToStart) {
rateTask(toRemove).then((value) => update()); //rateTask is a function that updates the task, it is removed from the list
}
if(direction == DismissDirection.startToEnd) {
dbProvider.update(/*code to update selected task*/).then((value) => update());
}
},
child: ListTile(
//ListTile details
),
);
},
);
}
Here is the problem (might be a wrong interpretation I'm still kind of new):
Dismissing a widget essentially removes it from the list. After the user dismisses a task, the task is "visually" removed from the list and the update() method is called, which calls setState(). Calling setState() causes the FutureBuilder to build again, but the dbProvider.getAllTasks() call is not completed by the time the FutureBuilder builds again. Therefore, the FutureBuilder passes the old snapshot, which causes the ListView to build again with the Task that just was dismissed. This causes the dismissed ListTile to appear momentarily after being dismissed, which looks creepy and wrong.
I have no idea how to fix this. Any help would be appreciated.
I was having the exact same issue, I was using sqflite which works with Futures so I ended up using the FutureBuilder alongside Dismissible for my ListView. The dismissed list item would remove then reappear for a frame then disappear again. I came across this question :
https://groups.google.com/forum/#!topic/flutter-dev/pC48MMVKJGc
which suggests removing the list item from the snapshot data itself:
return FutureBuilder<List<FolderModel>>(
future: Provider.of<FoldersProvider>(context).getFolders(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
final folder = snapshot.data[i];
return Dismissible(
onDismissed: (direction) {
snapshot.data.removeAt(i); // to avoid weird future builder issue with dismissible
Provider.of<FoldersProvider>(context, listen: false).deleteFolder(folder.id);
},
background: Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 1,
child: Container(
alignment: AlignmentDirectional.centerStart,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(15.0, 0.0, 0.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: UniqueKey(),
direction: DismissDirection.startToEnd,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
elevation: 1,
child: ListTile(
title: Text(folder.folderName),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.folder,
color: Theme.of(context).accentColor,
),
],
),
subtitle: folder.numberOfLists != 1
? Text('${folder.numberOfLists} items')
: Text('${folder.numberOfLists} item'),
onTap: () {},
),
),
);
},
);
},
);
and low and behold, it worked! Minimal changes to the code :)
Found a workaround for this by not using FutureBuilder and calling setState after the query is completed.
Instead of Future<List<Task>>, the state now contains a List<Task> which is declared as null.
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
DateTime now = DateTime.now();
List<Task> todayTasks;
//build
}
The update() function was changed as follows
void update() async {
Future<List<Task>> futureTasks = dbProvider.getByDate(now); //first make the query
futureTasks.then((data){
List<Task> tasks = new List<Task>();
for(int i = 0; i < data.length; i++) {
print(data[i].name);
tasks.add(data[i]);
}
setState(() {
todayTasks = tasks; //then setState and rebuild the widget
});
});
}
This way I the widget does not rebuild before the future is completed, which was the problem I had.
I removed the FutureBuilder completely, the Listview.builder just builds accordingly to the List stored in state.
#override
Widget build(BuildContext context) {
if(todayTasks == null) {
update();
return Center(
child: CircularProgressIndicator(),
);
} //make a query if the list has not yet been initialized
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets
pendingList(Task.filterByDone(false, todayTasks)),
],
);
}
This approach completely solved my problem, and I think its better than using a FutureBuilder in case the Future must be completed before the widget builds again.