Now it's time to finish the menu so it can use dynamic data. We will start by changing the ProductItem so it can render the image from a URL
ProductItem
class MenuItem extends StatelessWidget {
final Product product;
final Function onAdd;
const MenuItem({Key? key, required this.product, required this.onAdd})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4.0),
child: Card(
elevation: 4,
child: Column(
children: [
Image.network(product.imageUrl),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text("\$${product.price.toStringAsFixed(2)} ea"),
],
),
ElevatedButton(
onPressed: () {
onAdd(product);
},
child: const Text("Add"))
],
),
),
],
),
),
);
}
}
MenuPage
Then we redeign our MenuPage to loop through all the categories with a ListView.builder
and within it we use a for-in
to make other loop within their products. We also capture the onAdd
event we defined as an argument in ProductItem
and we add it to the cart.
class MenuPage extends StatelessWidget {
final DataManager dataManager;
const MenuPage({Key? key, required this.dataManager}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: FutureBuilder<List<Category>>(
future: dataManager.getMenu(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
// EACH CATEGORY STARTS HERE
var category = snapshot.data![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
top: 32.0, bottom: 8.0, left: 8.0),
child: Text(
category.name,
style: TextStyle(color: Colors.brown.shade400),
),
),
ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: category.products.length,
itemBuilder: (context, index) {
return MenuItem(
product: category.products[index],
onAdd: (p) => dataManager.cartAdd(p),
);
},
)
],
);
});
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
);
}
}
A version with media queries for responsive design using Wrap
can be like
class MenuPage extends StatelessWidget {
final DataManager dataManager;
const MenuPage({Key? key, required this.dataManager}) : super(key: key);
@override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
return Padding(
padding: const EdgeInsets.all(8.0),
child: FutureBuilder<List<Category>>(
future: dataManager.getMenu(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
// EACH CATEGORY STARTS HERE
var category = snapshot.data![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
top: 32.0, bottom: 8.0, left: 8.0),
child: Text(
category.name,
style: TextStyle(color: Colors.brown.shade400),
),
),
if (screenSize.width < 500)
// EACH MENU ITEM, Mobile Viewport
ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: category.products.length,
itemBuilder: (context, index) {
return MenuItem(
product: category.products[index],
onAdd: (p) => dataManager.cartAdd(p),
);
},
)
else
// EACH MENU ITEM, Large Viewport
Center(
child: Wrap(
alignment: WrapAlignment.spaceAround,
children: [
for (var product in category.products)
SizedBox(
width: 350,
child: MenuItem(
product: product,
onAdd: (p) => dataManager.cartAdd(p),
),
)
],
),
)
],
);
});
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
);
}
}