Back to Posts

Flutter RefreshIndicator: Implementing Pull-to-Refresh with JSON Data

8 min read
<div style="text-align: center;"> <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDMwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPCEtLSBSZWZyZXNoIEluZGljYXRvciBleGFtcGxlIC0tPgogIDxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRkZGIiBzdHJva2U9IiMwMDAiLz4KICA8dGV4dCB4PSIxNTAiIHk9IjEwMCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjMjEyMTIxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5QdWxsLXRvLVJlZnJlc2g8L3RleHQ+Cjwvc3ZnPg==" alt="Refresh Indicator Example" width="300" /> </div>

The RefreshIndicator widget in Flutter provides a standard way to implement pull-to-refresh functionality in your app. This guide will show you how to implement it effectively with JSON data, handle loading states, and create a smooth user experience.

Basic Implementation

1. Simple RefreshIndicator

class RefreshIndicatorExample extends StatefulWidget {
  const RefreshIndicatorExample({super.key});

  @override
  State<RefreshIndicatorExample> createState() => _RefreshIndicatorExampleState();
}

class _RefreshIndicatorExampleState extends State<RefreshIndicatorExample> {
  List<String> items = List.generate(20, (index) => 'Item $index');

  Future<void> _refreshData() async {
    // Simulate network delay
    await Future.delayed(const Duration(seconds: 2));
    setState(() {
      items = List.generate(20, (index) => 'Refreshed Item $index');
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _refreshData,
      child: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      ),
    );
  }
}

2. RefreshIndicator with JSON Data

class JsonRefreshIndicator extends StatefulWidget {
  const JsonRefreshIndicator({super.key});

  @override
  State<JsonRefreshIndicator> createState() => _JsonRefreshIndicatorState();
}

class _JsonRefreshIndicatorState extends State<JsonRefreshIndicator> {
  List<Map<String, dynamic>> items = [];
  bool isLoading = false;

  Future<void> _fetchData() async {
    try {
      final response = await http.get(
        Uri.parse('https://api.example.com/items'),
      );
      if (response.statusCode == 200) {
        final List<dynamic> data = json.decode(response.body);
        setState(() {
          items = data.cast<Map<String, dynamic>>();
        });
      } else {
        throw Exception('Failed to load data');
      }
    } catch (e) {
      // Handle error
    }
  }

  @override
  void initState() {
    super.initState();
    _fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _fetchData,
      child: isLoading
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: items.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(items[index]['title']),
                  subtitle: Text(items[index]['description']),
                );
              },
            ),
    );
  }
}

Advanced Features

1. Custom Refresh Indicator

class CustomRefreshIndicator extends StatefulWidget {
  const CustomRefreshIndicator({super.key});

  @override
  State<CustomRefreshIndicator> createState() => _CustomRefreshIndicatorState();
}

class _CustomRefreshIndicatorState extends State<CustomRefreshIndicator> {
  Future<void> _refreshData() async {
    await Future.delayed(const Duration(seconds: 2));
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _refreshData,
      color: Colors.blue,
      backgroundColor: Colors.white,
      strokeWidth: 2.0,
      displacement: 40.0,
      edgeOffset: 0.0,
      child: ListView.builder(
        itemCount: 20,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item $index'),
          );
        },
      ),
    );
  }
}

2. RefreshIndicator with Pagination

class PaginatedRefreshIndicator extends StatefulWidget {
  const PaginatedRefreshIndicator({super.key});

  @override
  State<PaginatedRefreshIndicator> createState() =>
      _PaginatedRefreshIndicatorState();
}

class _PaginatedRefreshIndicatorState extends State<PaginatedRefreshIndicator> {
  List<String> items = [];
  int page = 1;
  bool isLoading = false;
  bool hasMore = true;

  Future<void> _loadMore() async {
    if (isLoading || !hasMore) return;
    setState(() => isLoading = true);
    
    // Simulate API call
    await Future.delayed(const Duration(seconds: 1));
    final newItems = List.generate(10, (index) => 'Item ${items.length + index}');
    
    setState(() {
      items.addAll(newItems);
      isLoading = false;
      hasMore = items.length < 50; // Example limit
    });
  }

  Future<void> _refreshData() async {
    setState(() {
      page = 1;
      items = [];
      hasMore = true;
    });
    await _loadMore();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _refreshData,
      child: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          if (!isLoading &&
              hasMore &&
              scrollInfo.metrics.pixels ==
                  scrollInfo.metrics.maxScrollExtent) {
            _loadMore();
          }
          return true;
        },
        child: ListView.builder(
          itemCount: items.length + 1,
          itemBuilder: (context, index) {
            if (index == items.length) {
              return hasMore
                  ? const Center(child: CircularProgressIndicator())
                  : const SizedBox();
            }
            return ListTile(title: Text(items[index]));
          },
        ),
      ),
    );
  }
}

Best Practices

  1. Performance Optimization

    • Implement proper caching
    • Use appropriate loading indicators
    • Handle network errors gracefully
    • Optimize data fetching
  2. User Experience

    • Provide visual feedback
    • Show loading states
    • Handle errors appropriately
    • Maintain scroll position
  3. Error Handling

    • Implement retry mechanisms
    • Show error messages
    • Provide offline support
    • Handle network issues
  4. State Management

    • Use appropriate state management
    • Handle loading states
    • Manage data updates
    • Implement proper cleanup

Common Issues and Solutions

  1. Scroll Position

    // Maintain scroll position after refresh
    final ScrollController _scrollController = ScrollController();
    
    @override
    void dispose() {
      _scrollController.dispose();
      super.dispose();
    }
  2. Loading States

    // Show loading indicator
    RefreshIndicator(
      onRefresh: _refreshData,
      child: isLoading
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(...),
    )
  3. Error Handling

    // Handle errors gracefully
    Future<void> _refreshData() async {
      try {
        await _fetchData();
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: ${e.toString()}')),
        );
      }
    }

Conclusion

The RefreshIndicator widget is a powerful tool for implementing pull-to-refresh functionality in your Flutter app. Remember to:

  • Implement proper error handling
  • Provide visual feedback
  • Optimize performance
  • Follow best practices

Happy coding!