Flutter for Single-Page Scrollable Websites with Navigator 2.0 — Part 3: Scroll to Page

Cagatay Ulusoy
Geek Culture
Published in
6 min readJul 18, 2021

--

In the second part of this series, we discussed building a single-page scrollable website (SPSW) using a ListView whose sections are built lazily (on-demand) and have equal height. In this article, the sample app will be very similar to the previous app with one difference: In the ColorSections widget, instead of ListView , we will use PageView .

If you haven’t read the previous articles, I would strongly suggest starting from the first part of this series since the implementation details of the common widgets will not be mentioned in this part.

ColorSections

PageView widget is a scrollable list whose children have the same size which is equal to the viewport size by default. Each item in the list is called a page.

PageController extends the ScrollController . It allows setting which page is visible in a PageView using animateToPage , jumpToPage , nextPage, previousPage methods. We can set the page size relative to the viewport size using its viewportFraction property.

We can consider the PageView widget as a ListView but more tailored for items of equal size. We will build a PageView in a vertical scroll direction lazily using its builder constructor. By default, PageView widget locks the viewport to a certain page when a scrolling gesture ends. We will disable this feature by setting its pageSnapping property to false.

page snapping on
page snapping off

Listen to Color Code Update

We start listening for the colorCodeNotifier updates as soon as the ColorSections widget is inserted into the tree. At this moment, initState method is called, and the PageController doesn’t have any clients because the PageView is not yet laid out.

When we receive an update, if the PageController is attached to the PageView, and the source of the update is not scrolling, we provide the page index number to the animateToPage method of the PageController which scrolls to the destination index programmatically.

Notify Color Code

While the user is scrolling, we want to update the colorCodeNotifier listeners as the first visible color code section (trailingIndex) changes.

When we receive a UserScrollNotification , we get the current page index from the PageController , and set a new colorCode value for the corresponding page index.

Scrolling with trackpad

Resizing Browser Window

In our use case, we set each page’s height equal to the viewport height. When resizing the browser window we want to make sure that we should be able to show the page content even in the smallest height that browser allows. We will use viewportFraction property of the PageController to set a minimum height for pages.

The viewportFraction property is declared as final in PageController so, we can’t dynamically set its value. We will wrap the PageView with a LayoutBuilder class to be able create new PageController for the PageView according to the new size constraints.

… the framework calls the builder function at layout time and provides the parent widget’s constraints. This is useful when the parent constrains the child’s size and doesn’t depend on the child’s intrinsic size. The LayoutBuilder’s final size will match its child’s size.

We will calculate the viewportFraction for the available size according to this formulation:

  • We first decide on the minimum height that is enough to show our content in a page. In this sample, we set it to 800 px.
  • The builder function of LayoutBuilder will be called the first time the widget is laid out and when the parent widget passes different layout constraints. We need to construct a new PageController each time this method is called.
  • If the available height that the PageView can be laid out is greater than the _minPageHeight , then the viewportFraction equals to 1 which makes the page height equal to the viewport height.
  • If the available height is less than the _minPageHeight , we calculate the viewportFraction by dividing _minPageHeight by the available height. For example, if the viewport height equals to 400 px, we need to set the viewportFraction to 2 so that the page height will be 800 px.

Here is the full code for the ColorsSection widget:

Issues with Resizing

We actually don’t have any problem when the viewportFraction is set to less than or equal to 1. However, as of Flutter stable version 2.2.0, I experience issues using viewportFraction when it is greater than 1.

  • Clipped First and Last Pages

PageView widget is obsessed with snapping the content to the center of the viewport. When the viewportFraction that is greater than 1, it calculates _initalPageOffset value to be added to the minScrollContent and subtracted from the maxScrollContent to snap the content to the center. This decision leads to clipping the first and last pages.

  • Updating page index after the offset

To get the current page index from the PageController , we access the page property and floor it to an integer. For some reason, the page does not update until some offset value.

In the recording below, you can notice that the page index is correctly displayed when the viewportFraction is 1. As the browser height gets smaller, we set the viewportFraction to greater than 1. In the recording, the current page index is less than 1, although the PageView is actually displaying the 1st index.

Note that these two issues might be expected behavior or bugs. Please write in comments if you have solutions, or I am missing something.

Conclusion

In this article, we explored lazily building the sections of a single page scrollable website using a PageView widget. In the fourth part of this series, we will learn how to live with building all the sections at once in a SingleChildScrollView + Column .

You can run this sample app by right clicking on the main_003.02.dart file in the project source code. If you liked this article, please press the clap button, and star the Github repository of the sample apps.

--

--