The PageView widget allows user to transition between different screens in their Flutter application. As per Flutter doc,
PageView is a scrollable list that works page by page. Each child of a page view is forced to be the same size as the viewport.
Each child within a PageView
is typically constrained to the same size as the viewport, limiting flexibility in layout design. But what if we want our PageView
to dynamically adjust its size based on the content it contains?
In this article, we’ll implement the PageView
with children of different heights to make it flexible.
What we’ll achieve at the end of this blog?
You can find the full source code on GitHub.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
First, let's quickly have a look at the Pageview in Flutter, For simplicity, we have added a simple UI.
class StarterPageView extends StatelessWidget {
StarterPageView({super.key});
final List<int> items= List.generate(5, (index) => index);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 400,
child: PageView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color:Colors.black,
),
child: Wrap(
children: List.generate(index+1, (index) {
return Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.yellow,
),
height: 50,
child: ListTile(
title: Text('Page ${index+1}'),
),
);
}),
)
);
},
),
);
}
}
Here is the result,
The problem with the Pageview
in Flutter is it can’t have dynamic height.
If we have a tab that might have a height of 100 px and another 200 px then it can’t adjust the height to the currently displayed page. 😞😞
Our goal? To create a PageView
that adjusts its dimensions based on its content. So, Let’s start implementing it without wasting time.
We'll divide implementation into 5 simple steps to make each step easy to understand.
To implement dynamic sizing within our PageView, we'll start by implementing a widget that measures the size of the child and notifies the parent widget if the child's size is different than the previous one.
class SizeNotifierWidget extends StatefulWidget {
final Widget child;
final ValueChanged<Size> onSizeChange;
const SizeNotifierWidget({
super.key,
required this.child,
required this.onSizeChange,
});
@override
State<SizeNotifierWidget> createState() => _SizeNotifierWidgetState();
}
class _SizeNotifierWidgetState extends State<SizeNotifierWidget> {
Size? _oldSize;
@override
void didUpdateWidget(covariant SizeNotifierWidget oldWidget) {
WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return widget.child;
}
void _notifySize() {
final size = context.size;
if (size != null && _oldSize != size) {
_oldSize = size;
widget.onSizeChange(size);
}
}
}
This widget is used to monitor the size of its child widget and trigger a callback whenever that size changes. Let’s break it down,
It takes two required parameters,
child
: The child widget that this SizeNotifierWidget
will render.onSizeChange
: A callback function that will be invoked whenever the size of this widget changes.Inside the didUpdateWidget()
Method,
WidgetsBinding.instance.addPostFrameCallback()
. This callback ensures that _notifySize()
is called after the frame is rendered. This is necessary because the size of a widget is only available after it has been laid out on the screen.Now, we’ll use this widget to measure the size of the current item in the PageView
. So, whenever the child has a different height then it notifies about that.
class ThreePageScrollView extends StatefulWidget {
final Widget current;
const ThreePageScrollView({
super.key,
required this.current,
});
@override
State<ThreePageScrollView> createState() => _ThreePageScrollViewState();
}
class _ThreePageScrollViewState extends State<ThreePageScrollView> {
double? _currentItemHeight;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Opacity(
opacity: 0,
child: SizeNotifierWidget(
child: widget.current,
onSizeChange: (size) {
setState(() {
_currentItemHeight = size.height;
});
},
),
),
SizedBox(
height: _currentItemHeight ?? 0,
child: PageView(
children: [
widget.current,
],
)),
],
);
}
}
The code is pretty simple, here we've used the SizeNotifier
widget to measure the size of the current item and set that size to give it to PageView
.
Let's add a simple UI to see if it's working or not.
We'll add a simple UI to verify its functionality.
class FinalPageView extends StatelessWidget {
FinalPageView({super.key});
int itemCount = 1; // Number of items in the page view
@override
Widget build(BuildContext context) {
return ThreePageScrollView(
current: getItem(),
);
}
Widget getItem() {
return Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black,
),
child: Wrap(
children: List.generate(itemCount, (index) {
return Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.yellow,
),
height: 50,
child: ListTile(
title: Text('Page ${index + 1}'),
),
);
}),
));
}
}
The code is pretty straightforward, we have used our Custom PageView
and added a child from the getItem()
function. Let's see what it looks like,
Ah, got it!!
Now, let's make it swipeable.😃
We'll implement PageViewController
to manage PageView
and to make it scrollable.
class ThreePageScrollView extends StatefulWidget {
final Widget current;
final Widget? next;
final Widget? previous;
final Function(int)? onPageChanged;
const ThreePageScrollView({
super.key,
required this.current,
this.next,
this.previous,
required this.onPageChanged,
});
@override
State<ThreePageScrollView> createState() => _ThreePageScrollViewState();
}
class _ThreePageScrollViewState extends State<ThreePageScrollView> {
late PageController _pageController;
double? _currentItemHeight;
late int currentPage;
@override
void initState() {
super.initState();
currentPage = widget.previous == null ? 0 : 1;
_pageController = PageController(initialPage: currentPage);
_pageController.addListener(() {
final newPage = _pageController.page?.toInt();
if (newPage == null || newPage.toDouble() != _pageController.page) return;
if (newPage == currentPage) {
return;
}
_pageController.jumpToPage(1);
widget.onPageChanged!(newPage > currentPage ? 1 : -1);
currentPage = 1;
});
}
@override
void didUpdateWidget(covariant ThreePageScrollView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.previous != oldWidget.previous) {
final newPage = widget.previous == null ? 0 : 1;
if (newPage != currentPage) {
currentPage = newPage;
_pageController.jumpToPage(newPage);
}
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
....
SizedBox(
height: _currentItemHeight ?? 0,
child: PageView(
controller: _pageController,
physics: const FastPageViewScrollPhysics(),
children: [
if (widget.previous != null) widget.previous!,
widget.current,
if (widget.next != null) widget.next!,
],
)),
],
);
}
}
Here we have-
Before implementing PageContoller
, it's essential to have the previous and next child other than the current one to make it scrollable. So, We have set the following parameters,
next
: A widget representing the next page.previous
: A widget representing the previous page.onPageChanged(int)
: A callback function that is invoked when the page changes. It receives an integer parameter indicating the direction of the change (-1 for the previous, 1 for the next).Variables: _pageController
to manage PageView
and currentPage
variable is used to keep track of the current page index.
initState()
Method
_pageController
to detect page changes. When the page changes, it calls widget.onPageChanged
with the direction of the change.didUpdateWidget()
Method
Let’s use our custom ThreePageScrollView
into the UI.
class FinalPageView extends StatefulWidget {
...
@override
Widget build(BuildContext context) {
return ThreePageScrollView(
previous: itemCount > 1 ? getItem() : null,
next: getItem(),
current: getItem(),
onPageChanged: (int direction) {
setState(() {
if (itemCount == 1 && direction == -1) {
return;
}
itemCount += direction;
});
});
}
...
}
We've added the required parameters and updated itemCount
in the onPageChanged
callback. so we can check if it's adaptive or not according to the size of the current child.
Run the app, you'll see,
Cool 👌. And we have an Adaptive PageView
.
We've used FastPageViewScrollPhysics
, which is intended to be used with a PageView
widget for smoother and faster page scrolling behavior. Here is the code,
class FastPageViewScrollPhysics extends ScrollPhysics {
const FastPageViewScrollPhysics({super.parent});
@override
FastPageViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
return FastPageViewScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => const SpringDescription(
mass: 80,
stiffness: 100,
damping: 1,
);
}
FastPageViewScrollPhysics
extends the ScrollPhysics
class, which is the base class for physics used by scrolling widgets like ListView
, GridView
, and PageView
. We’ve made some changes to the overridden spring to achieve smoother and faster scrolling.
That’s it, we’re done with implementation. 👏
In this article, we’ve successfully implemented Adaptive PageView
to overcome the limitations of the Flutter PageView
Widget. By allowing our PageView
to dynamically adjust its size based on the content, we can create more engaging and user-friendly experiences for our users.
Happy coding! 🚀✨
Let's Work Together
Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.