Paginated ListView Infinite Scroll
Learn how to implement smooth infinite scroll experiences using the Paginated ListView widget. This guide covers real-world use cases including social media feeds, e-commerce product listings, and search results.

Basic Setup Pattern
Every Paginated ListView implementation follows this structure:
Create a paginated API that supports your chosen pagination strategy
Configure the widget with pagination parameters
Design the item template for displaying individual items
Add loading indicators for initial and subsequent loads
Handle empty and error states gracefully
Use Case 1: Social Media Feed
Build a Twitter/Instagram-style feed that loads posts infinitely as users scroll.
API Configuration
Endpoint: https://api.yourapp.com/posts
Pagination Type: Cursor-based (best for real-time feeds where content changes frequently)
Query Parameters:
cursor: ${nextCursor}
limit: 20Response Structure:
{
"posts": [
{
"id": "post_123",
"author": {
"name": "John Doe",
"avatar": "https://cdn.example.com/avatar.jpg",
"username": "@johndoe"
},
"content": "Just launched my new app!",
"image": "https://cdn.example.com/post_image.jpg",
"timestamp": "2024-11-05T10:30:00Z",
"likes": 234,
"comments": 45,
"isLiked": false
}
],
"nextCursor": "eyJpZCI6MTIzfQ==",
"hasMore": true
}Widget Configuration
Paginated ListView:
API Data Source: getPosts
First Page Key: null
Next Page Key: ${if(response.body.hasMore, response.body.nextCursor, null)}
Transform Items: ${response.body.posts}
Scroll Direction: Vertical
Show First Page Progress Indicator: true
Show New Page Progress Indicator: trueItem Template (children)
Create a variable to track liked posts:
Variable: likedPosts (List)
Default: []Add JavaScript function to toggle items in array:
function toggleInArray(data) {
var array = data[0];
var itemId = data[1];
var index = array.findIndex(id => id === itemId);
return index > -1 ? array.filter(id => id !== itemId) : [...array, itemId];
}Item template:
Container:
Padding: 12px
Column:
Cross Axis Alignment: Start
Gap: 8px
// Author Info
Row:
Gap: 12px
Avatar:
Image URL: ${currentItem.author.avatar}
Size: 40px
Column:
Cross Axis Alignment: Start
Text:
Text: ${currentItem.author.name}
Weight: Bold
Size: 16px
Text:
Text: ${currentItem.author.username}
Color: Gray
Size: 14px
// Post Content
Text:
Text: ${currentItem.content}
Size: 15px
Max Lines: null
// Post Image (if exists)
Conditional Builder:
Condition: ${isNotNull(currentItem.image)}
True:
Image:
URL: ${currentItem.image}
Fit: Cover
Border Radius: 8px
// Action Buttons
Row:
Main Axis Alignment: Space Between
Row:
Gap: 20px
// Like Button
Icon Button:
Icon: ${if(contains(likedPosts, currentItem.id), 'favorite', 'favorite_border')}
Color: ${if(contains(likedPosts, currentItem.id), 'red', 'gray')}
On Click:
Set State: likedPosts = ${js.eval('toggleInArray', likedPosts, currentItem.id)}
Call API: toggleLike
Arguments:
postId: ${currentItem.id}
Text:
Text: ${currentItem.likes}
// Comment Button
Icon Button:
Icon: chat_bubble_outline
Color: Gray
On Click:
Go To Page: CommentsPage
Text:
Text: ${currentItem.comments}
// Timestamp
Text:
Text: ${currentItem.timestamp}
Color: Gray
Size: 12px
DividerLoading Indicators
First Page Indicator:
Column:
Main Axis Alignment: Center
Cross Axis Alignment: Center
Padding: 40px
Circular Progress Indicator:
Size: 40px
Color: Primary
Sized Box:
Height: 16px
Text:
Text: "Loading feed..."
Color: GrayNew Page Indicator:
Container:
Padding: 16px
Alignment: Center
Circular Progress Indicator:
Size: 24px
Color: PrimaryPull to Refresh
Wrap the Paginated ListView in a Refresh Indicator:
Refresh Indicator:
On Refresh:
Call API: getPosts
Reset pagination: true
Child:
Paginated ListView:
// ... configuration as aboveUse Case 2: E-Commerce Product Catalog
Build a product listing page with infinite scroll, perfect for online stores with large inventories.
API Configuration
Endpoint: https://api.store.com/products
Pagination Type: Offset-based (predictable, works well for filtering/sorting)
Query Parameters:
offset: ${currentOffset}
limit: 24
category: ${selectedCategory}
sort: ${sortBy}Response Structure:
{
"products": [
{
"id": "prod_456",
"name": "Wireless Headphones",
"price": 99.99,
"originalPrice": 149.99,
"image": "https://cdn.store.com/headphones.jpg",
"rating": 4.5,
"reviews": 1234,
"inStock": true,
"discount": 33
}
],
"total": 5420,
"offset": 0,
"limit": 24
}Widget Configuration
Paginated ListView:
API Data Source: getProducts
First Page Key: 0
Next Page Key: ${if(lt(sum(response.body.offset, response.body.limit), response.body.total), sum(response.body.offset, 24), null)}
Transform Items: ${response.body.products}
Scroll Direction: VerticalItem Template (Grid-Style Layout)
Use a GridView crossAxisCount of 2 for a product grid, or wrap in a Container for list view:
Container:
Padding: 8px
Card:
Elevation: 2
Column:
Cross Axis Alignment: Start
// Product Image with Discount Badge
Stack:
Image:
URL: ${currentItem.image}
Height: 200px
Fit: Cover
// Discount Badge
Conditional Builder:
Condition: ${gt(currentItem.discount, 0)}
True:
Positioned:
Top: 8px
Right: 8px
Container:
Background: Red
Padding: 4px 8px
Border Radius: 4px
Text:
Text: ${concat(currentItem.discount, "% OFF")}
Color: White
Size: 12px
Weight: Bold
// Product Details
Container:
Padding: 12px
Column:
Cross Axis Alignment: Start
Gap: 8px
Text:
Text: ${currentItem.name}
Weight: SemiBold
Size: 16px
Max Lines: 2
Overflow: Ellipsis
// Rating
Row:
Gap: 4px
Icon:
Icon: star
Color: Orange
Size: 16px
Text:
Text: ${currentItem.rating}
Size: 14px
Text:
Text: ${concat("(", currentItem.reviews, ")")}
Color: Gray
Size: 12px
// Price
Row:
Gap: 8px
Text:
Text: ${concat("$", currentItem.price)}
Weight: Bold
Size: 18px
Color: Primary
Conditional Builder:
Condition: ${gt(currentItem.discount, 0)}
True:
Text:
Text: ${concat("$", currentItem.originalPrice)}
Size: 14px
Color: Gray
Decoration: Line Through
// Stock Status
Conditional Builder:
Condition: ${currentItem.inStock}
True:
Text:
Text: "In Stock"
Color: Green
Size: 12px
False:
Text:
Text: "Out of Stock"
Color: Red
Size: 12px
// Add to Cart Button
Button:
Text: "Add to Cart"
Enabled: ${currentItem.inStock}
On Click:
Call API: addToCart
Toast:
Message: "Added to cart!"Filter and Sort Integration
Add filters above the Paginated ListView:
Column:
// Filter Bar
Container:
Padding: 12px
Background: White
Row:
Main Axis Alignment: Space Between
// Category Dropdown
Dropdown:
Value: ${selectedCategory}
Items: ["All", "Electronics", "Clothing", "Home"]
On Change:
Set State: selectedCategory = ${currentValue}
Call API: getProducts
Reset pagination: true
// Sort Dropdown
Dropdown:
Value: ${sortBy}
Items: ["Popular", "Price: Low to High", "Price: High to Low", "Rating"]
On Change:
Set State: sortBy = ${currentValue}
Call API: getProducts
Reset pagination: true
// Product List
Paginated ListView:
// ... configurationUse Case 3: Search Results with Infinite Scroll
Implement a search interface that loads results progressively as users scroll.
API Configuration
Endpoint: https://api.search.com/search
Pagination Type: Page-based
Query Parameters:
query: ${searchQuery}
page: ${currentPage}
perPage: 15Response Structure:
{
"query": "laptop",
"results": [
{
"id": "result_789",
"title": "Best Laptop for 2024",
"snippet": "Discover the top-rated laptops...",
"url": "https://example.com/article",
"thumbnail": "https://cdn.example.com/thumb.jpg",
"source": "TechBlog",
"publishedDate": "2024-10-15"
}
],
"currentPage": 1,
"totalPages": 50,
"totalResults": 742
}Widget Configuration
Paginated ListView:
API Data Source: searchResults
First Page Key: 1
Next Page Key: ${if(lt(response.body.currentPage, response.body.totalPages), sum(response.body.currentPage, 1), null)}
Transform Items: ${response.body.results}Search Interface
Column:
// Search Bar
Container:
Padding: 12px
Background: White
Text Form Field:
Hint: "Search..."
On Change:
Set State: searchQuery = ${currentValue}
On Submit:
Call API: searchResults
Reset pagination: true
// Results Count
Conditional Builder:
Condition: ${isNotNull(searchResults)}
True:
Container:
Padding: 8px 12px
Text:
Text: ${concat(searchResults.totalResults, " results found")}
Color: Gray
Size: 14px
// Search Results
Paginated ListView:
// ... configurationItem Template
Container:
Padding: 12px
Row:
Gap: 12px
Cross Axis Alignment: Start
// Thumbnail
Image:
URL: ${currentItem.thumbnail}
Width: 100px
Height: 100px
Fit: Cover
Border Radius: 8px
// Content
Column:
Cross Axis Alignment: Start
Gap: 6px
Expanded: true
Text:
Text: ${currentItem.title}
Weight: SemiBold
Size: 16px
Max Lines: 2
Overflow: Ellipsis
Text:
Text: ${currentItem.snippet}
Color: Gray
Size: 14px
Max Lines: 3
Overflow: Ellipsis
Row:
Gap: 8px
Text:
Text: ${currentItem.source}
Color: Primary
Size: 12px
Text:
Text: "•"
Color: Gray
Text:
Text: ${currentItem.publishedDate}
Color: Gray
Size: 12px
DividerEmpty State
Add an empty state when no results are found:
Conditional Builder:
Condition: ${and(isNotNull(searchResults), isEqual(searchResults.results.length, 0))}
True:
Container:
Padding: 40px
Alignment: Center
Column:
Main Axis Alignment: Center
Cross Axis Alignment: Center
Gap: 16px
Icon:
Icon: search_off
Size: 64px
Color: Gray
Text:
Text: "No results found"
Weight: Bold
Size: 18px
Text:
Text: ${concat("Try searching for something else")}
Color: Gray
Align: CenterPerformance Optimization Tips
1. Optimize Item Templates
Keep item widgets lightweight:
// ❌ Avoid: Heavy nested structures
Column
└─ Container
└─ Card
└─ Container
└─ Row
└─ ...
// ✅ Better: Flat structure
Card
└─ Row
└─ ...2. Use Appropriate Page Sizes
Balance between performance and UX:
Small pages (10-15 items): Faster initial load, more frequent API calls
Medium pages (20-30 items): Good balance for most use cases
Large pages (50+ items): Fewer API calls, slower initial load
3. Add Skeleton Loaders
Improve perceived performance with skeleton screens:
firstPageLoadingIndicator:
Column:
Children: [SkeletonItem, SkeletonItem, SkeletonItem]
SkeletonItem:
Container:
Padding: 12px
Row:
Gap: 12px
Container:
Width: 100px
Height: 100px
Background: Gray[200]
Border Radius: 8px
Column:
Gap: 8px
Container:
Width: 200px
Height: 16px
Background: Gray[200]
Border Radius: 4px
Container:
Width: 150px
Height: 14px
Background: Gray[200]
Border Radius: 4pxError Handling Patterns
Network Error State
On API Error:
Show Dialog:
Title: "Connection Error"
Content: "Failed to load data. Please check your connection."
Actions:
- Button:
Text: "Retry"
On Click:
Call API: getPosts
Retry: trueBest Practices Summary
Choose the right pagination type for your data:
Cursor-based for real-time feeds
Offset-based for static catalogs
Page-based for general use cases
Always show loading states to keep users informed
Handle edge cases:
Empty results
Network errors
End of data
Optimize for performance:
Keep item templates simple
Use appropriate page sizes
Implement image caching
Provide user controls:
Pull to refresh
Filter and sort options
Search functionality
Test thoroughly:
Slow network conditions
Large datasets
Error scenarios
Related Documentation
Paginated ListView - Complete widget reference
ListView - For non-paginated lists
API Calls - Learn about API integration
Conditional Builder - For conditional rendering
Last updated