I have a FutureBuilder that gets data from an API, I'll be reloading this data a lot, so the FutureBuilder will refresh a lot.
The problem I'm having is the entire widget structure gets rebuilt every time, and the ListViews and scrolls get reset back to their original positions. I want only the OrderStack to be rebuilt in place. Is this possible? If so how?
class ServingStationScreen extends StatefulWidget {
const ServingStationScreen({Key? key}) : super(key: key);
@override
ServingStationScreenState createState() => ServingStationScreenState();
}
class ServingStationScreenState extends State<ServingStationScreen> {
late Future<Map<String, List<Order>>> ordersFuture;
@override
void initState() {
super.initState();
ordersFuture = getOrders();
}
Future<Map<String, List<Order>>> getOrders() async {
try {
var orders = (await APIService().getOrders())!;
return {
'ordered': orders.where((order) {
return order.itemsOrdered!.isNotEmpty;
}).toList(),
'preparingAndAwaitingCancellation': orders.where((order) {
return order.itemsPreparingAndAwaitingCancellation!.isNotEmpty;
}).toList(),
};
} catch (e) {
return Future.error(e);
}
}
final List<String> emptyImages = [
'assets/svg/barbecue.svg',
'assets/svg/beer.svg',
'assets/svg/breakfast.svg',
'assets/svg/cooking.svg',
'assets/svg/fresh-drink.svg',
'assets/svg/ice-cream.svg',
'assets/svg/mint-tea.svg',
'assets/svg/pizza.svg',
'assets/svg/special-event.svg',
'assets/svg/tea.svg',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: Navigation(
allowBack: false,
activePage: 'Serving Station',
restaurantName: 'Test Restaurant',
actions: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
onPressed: () {
setState(() {
ordersFuture = getOrders();
});
// Get.to(() => const ServingStationHistoryScreen());
},
icon: const Icon(
Icons.history_rounded,
color: Colors.white,
size: 26.0,
),
),
),
],
),
extendBodyBehindAppBar: true,
backgroundColor: const Color.fromRGBO(21, 21, 32, 1),
body: FutureBuilder(
future: ordersFuture,
builder: (context, snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return const CircularProgressIndicator(
color: Color.fromRGBO(93, 194, 188, 1),
);
case ConnectionState.done:
if(snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.only(bottom: 100),
child: Opacity(
opacity: 0.5,
child: SvgPicture.asset(
'assets/svg/server.svg',
height: SizerUtil.deviceType == DeviceType.tablet ? 500 : 200,
theme: const SvgTheme(
currentColor: Color.fromRGBO(93, 194, 188, 1),
),
),
),
),
Text(
'Service Unreachable',
textAlign: TextAlign.center,
style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 30 : 18, color: Colors.white),
),
Text(
'Error: ${snapshot.error}',
textAlign: TextAlign.center,
style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 15 : 10, color: Colors.red),
),
],
),
);
} else {
final data = snapshot.data as Map<String, List<Order>>;
return data['preparingAndAwaitingCancellation']!.isNotEmpty || data['ordered']!.isNotEmpty ? SafeArea(
bottom: false,
child: ListView(
shrinkWrap: false,
children: [
data['preparingAndAwaitingCancellation']!.isNotEmpty ? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(top: 20, left: 20, bottom: 20),
child: Row(
children: [
const Text('Current Items',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
Container(
margin: const EdgeInsets.only(left: 10, right: 10),
padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 2),
decoration: BoxDecoration(
color: Colors.yellow[200],
border: Border.all(
color: Colors.transparent,
),
borderRadius: const BorderRadius.all(Radius.circular(20))
),
child: const Text(
'Preparing',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Container(
padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 2),
decoration: BoxDecoration(
color: Colors.red[200],
border: Border.all(
color: Colors.transparent,
),
borderRadius: const BorderRadius.all(Radius.circular(20))
),
child: const Text(
'Awaiting Cancellation',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: data['preparingAndAwaitingCancellation']!.length * 500 < 2500 ? data['preparingAndAwaitingCancellation']!.length * 500 : 2500,
child: MasonryGridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: data['preparingAndAwaitingCancellation']!.length < 5 ? data['preparingAndAwaitingCancellation']!.length : 5,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
shrinkWrap: true,
itemCount: data['preparingAndAwaitingCancellation']!.length,
padding: const EdgeInsets.only(left: 20, right: 20),
itemBuilder: (context, index) {
return OrderStack(
order: data['preparingAndAwaitingCancellation']![index],
filter: OrderStatusFilter.preparingAndAwaitingCancellation,
);
},
),
),
),
],
) : Container(),
data['ordered']!.isNotEmpty ? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(top: 20, left: 20, bottom: 20),
child: Row(
children: [
const Text('New Items',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
Container(
margin: const EdgeInsets.only(left: 10, right: 10),
padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 2),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.transparent,
),
borderRadius: const BorderRadius.all(Radius.circular(20))
),
child: const Text(
'Ordered',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: data['ordered']!.length * 500 < 2500 ? data['ordered']!.length * 500 : 2500,
child: MasonryGridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: data['ordered']!.length < 5 ? data['ordered']!.length : 5,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
shrinkWrap: true,
itemCount: data['ordered']!.length,
padding: const EdgeInsets.only(left: 20, right: 20),
itemBuilder: (context, index) {
return OrderStack(
order: data['ordered']![index],
filter: OrderStatusFilter.ordered,
);
},
),
),
),
],
) : Container(),
],
),
) : Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.only(bottom: 100),
child: Opacity(
opacity: 0.5,
child: SvgPicture.asset(
emptyImages.elementAt(Random().nextInt(emptyImages.length)),
height: SizerUtil.deviceType == DeviceType.tablet ? 500 : 200,
theme: const SvgTheme(
currentColor: Color.fromRGBO(93, 194, 188, 1),
),
),
),
),
Text(
'There are currently no pending orders.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 30 : 18, color: Colors.white),
),
],
),
);
}
}
},
),
);
}
}
CodePudding user response:
You should drop your FutureBuilder
as far down your widget tree as possible, wrapping only the widgets that are affected by that future. It looks like you have a SingleChildScrollView
that holds the OrderStack
, so you might wrap only that widget in the FutureBuilder
. Then, only that portion of the UI is updated when the ordersFuture
completes.
Also, if this is essentially a stream of data, then consider using StreamBuilder
instead - FutureBuilder
is typically used to wait for one-time completion of a task, once (e.g. loading the data to display in a list), not necessarily to refresh it.