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.

An example showing infinite scroll in a social media feed with smooth pagination.
Create seamless infinite scroll experiences that load data on demand.

Basic Setup Pattern

Every Paginated ListView implementation follows this structure:

  1. Create a paginated API that supports your chosen pagination strategy

  2. Configure the widget with pagination parameters

  3. Design the item template for displaying individual items

  4. Add loading indicators for initial and subsequent loads

  5. 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: 20

Response 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: true

Item 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
    
    Divider

Loading 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: Gray

New Page Indicator:

Container:
  Padding: 16px
  Alignment: Center
  
  Circular Progress Indicator:
    Size: 24px
    Color: Primary

Pull 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 above

Use 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: Vertical

Item 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:
    // ... configuration

Use 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: 15

Response 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:
    // ... configuration

Item 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
  
  Divider

Empty 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: Center

Performance 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: 4px

Error 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: true

Best Practices Summary

  1. 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

  2. Always show loading states to keep users informed

  3. Handle edge cases:

    • Empty results

    • Network errors

    • End of data

  4. Optimize for performance:

    • Keep item templates simple

    • Use appropriate page sizes

    • Implement image caching

  5. Provide user controls:

    • Pull to refresh

    • Filter and sort options

    • Search functionality

  6. Test thoroughly:

    • Slow network conditions

    • Large datasets

    • Error scenarios

Last updated