I'm having quite lot of problems to render the items of a ListViewBuilder with a dynamic height, meaning that the height will change accordingly to the number of items that i want to render. Moreover, the listview is NeverScrollable since i wrapped it with a SingleChildScrollView to scroll the listView together with other widgets as a unique widget. Finally, the shrinkWrap is set to True for the listViewBuilder. The problem is that if i fix the height of the TabBarView, which contains the ListViewBuilder, to a value which is bigger than the all items summed up height, then i will be left with white space! On the other hand, if the height is smaller, then some items aren't rendered!!! Do you have any solutions for this? Thanks!
Below the code:
- This the HomePage: i have a Column with a Container for the search bar and a FoodPageView() (this last one is expanded) as children:
const HomePage({
Key? key,
}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
currentIndex: 1,
selectedIconTheme: const IconThemeData(
color: Colors.blue,
),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart),
label: 'Cart',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Me',
),
],
),
body: SafeArea(
child: Column(
children: [
Container(
margin: const EdgeInsets.only(
top: 20,
left: 10,
right: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.blue[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
width: 0,
style: BorderStyle.none,
),
),
contentPadding: const EdgeInsets.only(
left: 20,
),
hintText: "Search store",
),
),
),
Container(
margin: const EdgeInsets.only(
left: 10,
),
height: Dimensions.height50,
width: Dimensions.width50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Colors.amberAccent,
),
child: const Icon(
Icons.search_outlined,
),
)
],
),
),
SizedBox(
height: Dimensions.height20,
),
const Expanded(child: FoodPageView()),
],
),
),
);
}
}
- The FoodPageView() implementation: it contains a Column with children a PageViewBuilder, a DotsIndicator and finally a custom NavigationBarTab()
const FoodPageView({Key? key}) : super(key: key);
@override
State<FoodPageView> createState() => _FoodPageViewState();
}
class _FoodPageViewState extends State<FoodPageView> {
final PageController _pageController = PageController(
viewportFraction: 0.85,
);
double _currPageValue = 0.0;
final double _scaleFactor = 0.8;
final int _height = 300;
@override
void initState() {
super.initState();
_pageController.addListener(() {
setState(() {
_currPageValue = _pageController.page!;
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: const ScrollPhysics(),
child: Column(
children: [
SizedBox(
height: Dimensions.height290,
child: BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
final List<Product> productsPromos = state.products
.where((product) => product.hasPromo == true)
.toList();
return PageView.builder(
controller: _pageController,
physics: const ScrollPhysics(),
itemCount: productsPromos.length,
itemBuilder: ((context, index) {
Matrix4 matrix = Matrix4.identity();
if (index == _currPageValue.floor()) {
final double currScale =
1 - (_currPageValue - index) * (1 - _scaleFactor);
final double currTrans = _height * (1 - currScale) / 2;
matrix = Matrix4.diagonal3Values(1, currScale, 1)
..setTranslationRaw(0, currTrans, 0);
} else if (index == _currPageValue.floor() 1) {
final double currScale = _scaleFactor
(_currPageValue - index 1) * (1 - _scaleFactor);
final double currTrans = _height * (1 - currScale) / 2;
matrix = Matrix4.diagonal3Values(1, currScale, 1)
..setTranslationRaw(0, currTrans, 0);
} else if (index == _currPageValue.floor() - 1) {
final double currScale =
1 - (_currPageValue - index) * (1 - _scaleFactor);
final double currTrans = _height * (1 - currScale) / 2;
matrix = Matrix4.diagonal3Values(1, currScale, 1)
..setTranslationRaw(0, currTrans, 0);
} else {
const double currScale = 0.8;
final double currTrans = _height * (1 - _scaleFactor) / 2;
matrix = Matrix4.diagonal3Values(1, currScale, 1)
..setTranslationRaw(0, currTrans, 0);
}
return Transform(
transform: matrix,
child: Stack(
children: [
Container(
height: Dimensions.height200,
margin: const EdgeInsets.only(
right: 10,
),
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: AssetImage(productsPromos[index].image),
),
borderRadius: BorderRadius.circular(20),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: Dimensions.height100,
margin: const EdgeInsets.only(
left: 30,
right: 30,
bottom: 15,
),
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Colors.grey,
blurRadius: 5,
offset: Offset(0, 5),
),
BoxShadow(
color: Colors.white,
blurRadius: 0,
offset: Offset(-5, 0),
),
BoxShadow(
color: Colors.white,
blurRadius: 0,
offset: Offset(5, 0),
),
],
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.only(
left: 10,
top: 10,
bottom: 10,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
productsPromos[index].name,
style: const TextStyle(
fontSize: 20,
),
),
const SizedBox(height: 10),
const Text(
'Offer',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
Text(
'${productsPromos[index].promo!.percentagePromo}% Off',
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
]),
const Icon(
Icons.keyboard_arrow_right_outlined,
size: 40,
),
],
),
),
),
),
],
),
);
}),
);
},
),
),
BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
final List<Product> productsPromos = state.products
.where((product) => product.hasPromo == true)
.toList();
return DotsIndicator(
dotsCount: productsPromos.length,
position: _currPageValue,
decorator: DotsDecorator(
activeSize: Size(Dimensions.width20, Dimensions.height10),
activeShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
5,
),
),
),
);
},
),
SizedBox(height: Dimensions.height5),
const NavigationBarTab(),
],
),
);
}
}
- The NavigationBarTab() page below: it is a Column with a Tab Item Menu followed by the corresponding tabView page. Each tabView page is a ListViewBuilder FoodListView()
const NavigationBarTab({Key? key}) : super(key: key);
@override
State<NavigationBarTab> createState() => _NavigationBarTabState();
}
class _NavigationBarTabState extends State<NavigationBarTab>
with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final TabController tabController = TabController(
length: 4,
vsync: this,
);
return Column(
children: [
SizedBox(
height: 30,
child: TabBar(
isScrollable: false,
controller: tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
tabs: const [
Tab(
text: 'Pizza',
),
Tab(
text: 'Specials',
),
Tab(
text: 'Desserts',
),
Tab(
text: 'Drinks',
),
],
),
),
SizedBox(
height: 700,
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: tabController,
children: const [
Expanded(child: FoodListView()),
Expanded(child: FoodListView()),
Expanded(child: FoodListView()),
Expanded(child: FoodListView()),
],
),
)
],
);
}
}
- Finally the FoodListView() page: a ListViewBuilder with NeverScrollable physics and shrinkWrap to true.
class FoodListView extends StatefulWidget {
const FoodListView({Key? key}) : super(key: key);
@override
State<FoodListView> createState() => _FoodListViewState();
}
class _FoodListViewState extends State<FoodListView> {
final PageController _pageController = PageController();
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
final List<Product> products = state.products
.where(
(element) => element.hasPromo == false,
)
.toList();
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: products.length,
itemBuilder: ((context, index) {
return FoodCard(
index: index,
products: products,
);
}),
);
},
);
}
}
- The FoodCard() is a Container with fixed height.
final int index;
final List<Product> products;
const FoodCard({
Key? key,
required this.index,
required this.products,
}) : super(key: key);
@override
State<FoodCard> createState() => _FoodCardState();
}
class _FoodCardState extends State<FoodCard> {
@override
Widget build(BuildContext context) {
return BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
return Container(
height: 125,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.grey,
),
),
margin: const EdgeInsets.only(
left: 5,
right: 5,
top: 5,
),
child: Row(
children: [
Container(
margin: const EdgeInsets.only(
top: 5,
left: 5,
bottom: 5,
),
height: Dimensions.height120,
width: Dimensions.width120,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: AssetImage(
widget.products[widget.index].image,
),
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
),
),
Expanded(
child: Container(
padding: const EdgeInsets.only(
left: 5,
),
margin: const EdgeInsets.only(
top: 5,
bottom: 5,
right: 5,
),
height: Dimensions.height120,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.products[widget.index].name,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: Dimensions.height10,
),
Expanded(
child: Stack(children: [
SizedBox(
height: Dimensions.height150,
width: Dimensions.width210,
child: Text(
widget.products[widget.index].description,
maxLines: 4,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
),
),
]),
),
SizedBox(
height: Dimensions.height5,
),
Text(
'\$ ${widget.products[widget.index].price}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
Container(
height: Dimensions.height30,
width: Dimensions.width30,
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.shopping_cart,
color: Colors.white,
),
)
],
),
),
),
],
),
);
},
);
}
}
CodePudding user response:
When you try two use Expanded
widget indirectly inside column
or row
widget, you you should wrap the parent with Expanded
too, so remove the SizedBox
and do this:
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: tabController,
children: const [
FoodListView(),
FoodListView(),
FoodListView(),
FoodListView(),
],
),
),
CodePudding user response:
You can either move the whole widget inside a expanded. Which is considered the most used way of doing it. Like this ->
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: _yourController,
children: const [
FoodListView(),
FoodListView(),
FoodListView(),
FoodListView(),
],
),
),
For this you'll have to remove the ScrollView you're using. Or In your wayyy, What you can try doing is, Enabling -> "shrinkWrap: true," inside your ListView.Builder(). And in both ways, you should be able to get rid of the fixed height you're providing now.