Flutter – How to Integrate REST API and Display Data in ListView
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
-
Error Handling
- Implement proper error handling for network failures
- Show meaningful error messages to users
- Provide retry options when appropriate
-
Performance
- Use pagination for large datasets
- Implement proper caching mechanisms
- Optimize image loading and list rendering
-
User Experience
- Show loading indicators during data fetch
- Implement pull-to-refresh functionality
- Add smooth animations for state changes
-
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.