A standard scrollable content area and a TikTok-style full-screen swipeable feed are built from two genuinely different Flutter widgets — SingleChildScrollView (or ListView) for the former, PageView for the latter.
A basic scroll view for content taller than the screen
SingleChildScrollView(
child: Column(
children: [
// many widgets, together taller than the screen
Image.asset('assets/banner.jpg'),
const Text('Long article content...'),
// ...
],
),
)
Column alone doesn't scroll — it simply overflows and throws a rendering error if its children exceed the available height. Wrapping it in SingleChildScrollView is what makes that same content scrollable.
ListView, for a long list of similar items
ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(title: Text(products[index].name));
},
)
ListView.builder only builds the items currently visible on screen (plus a small buffer), unlike a SingleChildScrollView wrapping a Column, which builds every child up front regardless of visibility — this matters significantly for a long list, where building thousands of off-screen widgets up front would waste memory and hurt performance.
A horizontal scroll view
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: categories.map((c) => CategoryChip(category: c)).toList(),
),
)
PageView, for a full-screen swipeable feed
PageView.builder(
scrollDirection: Axis.vertical,
itemCount: videos.length,
itemBuilder: (context, index) {
return VideoPlayerScreen(video: videos[index]);
},
)
PageView snaps to show exactly one full child at a time, with a swipe gesture moving to the next or previous one — scrollDirection: Axis.vertical is what produces the TikTok-style up/down swipe behavior, as opposed to the horizontal swipe more commonly seen in an onboarding-screen carousel.
Detecting which page is currently visible in a PageView
final PageController _pageController = PageController();
int _currentPage = 0;
PageView.builder(
controller: _pageController,
scrollDirection: Axis.vertical,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
itemBuilder: (context, index) => VideoPlayerScreen(video: videos[index]),
)
onPageChanged fires whenever the currently visible page changes — genuinely essential for a video feed, since it's exactly the signal needed to pause the previous video and start playing the newly visible one.
Combining infinite scroll pagination with ListView
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
_loadMoreItems();
}
});
}
Following the same underlying concept as the web-based infinite scroll pattern covered elsewhere on this site, listening for the scroll position approaching the maximum extent is how a Flutter list triggers loading its next page of data as the user nears the bottom.