Back to Posts

Flutter – How to Integrate REST API and Display Data in ListView

17 min read

Integrating REST APIs and displaying data in a ListView is a common requirement in modern mobile applications. This guide will show you how to fetch data from APIs, handle responses, and create beautiful list interfaces in Flutter.

Setting Up Dependencies

First, add the HTTP package to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0

Creating the Data Model

Let's create a model class to handle our data:

class Post {
  final int id;
  final String title;
  final String body;

  Post({
    required this.id,
    required this.title,
    required this.body,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

Basic API Integration

Here's a simple example using the JSONPlaceholder API:

import 'package:http/http.dart' as http;
import 'dart:convert';

class PostsService {
  static const String baseUrl = 'https://jsonplaceholder.typicode.com';

  Future<List<Post>> fetchPosts() async {
    try {
      final response = await http.get(Uri.parse('$baseUrl/posts'));
      
      if (response.statusCode == 200) {
        final List<dynamic> jsonData = json.decode(response.body);
        return jsonData.map((json) => Post.fromJson(json)).toList();
      } else {
        throw Exception('Failed to load posts');
      }
    } catch (e) {
      throw Exception('Error fetching posts: $e');
    }
  }
}

Building the ListView

Create a widget to display the fetched data:

class PostsList extends StatefulWidget {
  @override
  _PostsListState createState() => _PostsListState();
}

class _PostsListState extends State<PostsList> {
  final PostsService _postsService = PostsService();
  late Future<List<Post>> _futureData;

  @override
  void initState() {
    super.initState();
    _futureData = _postsService.fetchPosts();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Posts'),
      ),
      body: FutureBuilder<List<Post>>(
        future: _futureData,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                final post = snapshot.data![index];
                return Card(
                  margin: EdgeInsets.all(8),
                  child: ListTile(
                    title: Text(
                      post.title,
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    subtitle: Text(post.body),
                    leading: CircleAvatar(
                      child: Text(post.id.toString()),
                    ),
                  ),
                );
              },
            );
          } else if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          }
          return Center(
            child: CircularProgressIndicator(),
          );
        },
      ),
    );
  }
}

Advanced Implementation

Adding Pull-to-Refresh

Implement refresh functionality:

class AdvancedPostsList extends StatefulWidget {
  @override
  _AdvancedPostsListState createState() => _AdvancedPostsListState();
}

class _AdvancedPostsListState extends State<AdvancedPostsList> {
  final PostsService _postsService = PostsService();
  List<Post> _posts = [];
  bool _isLoading = false;
  String? _error;

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

  Future<void> _loadPosts() async {
    if (_isLoading) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final posts = await _postsService.fetchPosts();
      setState(() {
        _posts = posts;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Posts'),
      ),
      body: RefreshIndicator(
        onRefresh: _loadPosts,
        child: _error != null
            ? _buildErrorWidget()
            : _posts.isEmpty && _isLoading
                ? _buildLoadingWidget()
                : _buildListView(),
      ),
    );
  }

  Widget _buildErrorWidget() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Error: $_error'),
          ElevatedButton(
            onPressed: _loadPosts,
            child: Text('Retry'),
          ),
        ],
      ),
    );
  }

  Widget _buildLoadingWidget() {
    return Center(
      child: CircularProgressIndicator(),
    );
  }

  Widget _buildListView() {
    return ListView.builder(
      itemCount: _posts.length,
      itemBuilder: (context, index) {
        final post = _posts[index];
        return Card(
          margin: EdgeInsets.symmetric(
            horizontal: 16,
            vertical: 8,
          ),
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  post.title,
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  post.body,
                  style: TextStyle(
                    color: Colors.grey[700],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

Implementing Pagination

Add support for loading more data:

class PaginatedPostsList extends StatefulWidget {
  @override
  _PaginatedPostsListState createState() => _PaginatedPostsListState();
}

class _PaginatedPostsListState extends State<PaginatedPostsList> {
  final ScrollController _scrollController = ScrollController();
  final List<Post> _posts = [];
  bool _isLoading = false;
  bool _hasMore = true;
  int _page = 1;
  final int _limit = 20;

  @override
  void initState() {
    super.initState();
    _loadMorePosts();
    _scrollController.addListener(_onScroll);
  }

  Future<void> _loadMorePosts() async {
    if (_isLoading || !_hasMore) return;

    setState(() {
      _isLoading = true;
    });

    try {
      final url = 'https://jsonplaceholder.typicode.com/posts?_page=$_page&_limit=$_limit';
      final response = await http.get(Uri.parse(url));
      
      if (response.statusCode == 200) {
        final List<dynamic> newPosts = json.decode(response.body);
        
        if (newPosts.isEmpty) {
          setState(() {
            _hasMore = false;
          });
        } else {
          setState(() {
            _posts.addAll(
              newPosts.map((json) => Post.fromJson(json)),
            );
            _page++;
          });
        }
      }
    } catch (e) {
      print('Error loading more posts: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  void _onScroll() {
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _loadMorePosts();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Posts'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: _posts.length + (_hasMore ? 1 : 0),
        itemBuilder: (context, index) {
          if (index >= _posts.length) {
            return _buildLoader();
          }

          final post = _posts[index];
          return Card(
            margin: EdgeInsets.all(8),
            child: ListTile(
              title: Text(post.title),
              subtitle: Text(post.body),
            ),
          );
        },
      ),
    );
  }

  Widget _buildLoader() {
    return Container(
      padding: EdgeInsets.symmetric(vertical: 16),
      alignment: Alignment.center,
      child: CircularProgressIndicator(),
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

Error Handling and Loading States

Implement proper error handling and loading states:

class ErrorHandler {
  static Widget buildErrorWidget(String error, VoidCallback onRetry) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.error_outline,
            color: Colors.red,
            size: 60,
          ),
          SizedBox(height: 16),
          Text(
            'Error: $error',
            textAlign: TextAlign.center,
            style: TextStyle(
              color: Colors.red,
            ),
          ),
          SizedBox(height: 16),
          ElevatedButton(
            onPressed: onRetry,
            child: Text('Retry'),
          ),
        ],
      ),
    );
  }

  static Widget buildLoadingWidget() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(),
          SizedBox(height: 16),
          Text('Loading...'),
        ],
      ),
    );
  }
}

Best Practices

  1. Error Handling

    • Implement proper error handling for network failures
    • Show meaningful error messages to users
    • Provide retry options when appropriate
  2. Performance

    • Use pagination for large datasets
    • Implement proper caching mechanisms
    • Optimize image loading and list rendering
  3. User Experience

    • Show loading indicators during data fetch
    • Implement pull-to-refresh functionality
    • Add smooth animations for state changes
  4. Code Organization

    • Separate API logic from UI code
    • Use proper state management
    • Follow clean architecture principles

Common Issues and Solutions

1. Handling Network Timeouts

Future<List<Post>> fetchPostsWithTimeout() async {
  try {
    final response = await http
        .get(Uri.parse('$baseUrl/posts'))
        .timeout(Duration(seconds: 10));
    // Process response
  } on TimeoutException {
    throw Exception('Request timed out');
  } catch (e) {
    throw Exception('Failed to load posts');
  }
}

2. Implementing Caching

class PostsCache {
  static const String key = 'cached_posts';
  
  Future<void> cachePosts(List<Post> posts) async {
    final prefs = await SharedPreferences.getInstance();
    final jsonData = posts.map((post) => post.toJson()).toList();
    await prefs.setString(key, json.encode(jsonData));
  }

  Future<List<Post>?> getCachedPosts() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = prefs.getString(key);
    
    if (jsonString != null) {
      final List<dynamic> jsonData = json.decode(jsonString);
      return jsonData.map((json) => Post.fromJson(json)).toList();
    }
    return null;
  }
}

Conclusion

Integrating REST APIs with ListView in Flutter requires careful consideration of error handling, performance, and user experience. By following these patterns and best practices, you can create robust and efficient list interfaces that handle data fetching gracefully. Remember to implement proper error handling, loading states, and pagination for the best user experience.