Flutter DataTable with Live API Data: Complete Tutorial (2026)
Last Updated: | Author: Deep Singh
Tables are one of the most useful ways to present structured data — user lists, order histories, inventory, log entries. And when that data comes from an API, you want the table to load smoothly and show a friendly "loading" state while it fetches.
In this tutorial, you'll build a Flutter app that fetches a list of users from a public REST API and displays them in a DataTable widget. We'll cover JSON parsing, the loading state, and the basic layout you need to display real data from the network.
Before You Start
Make sure you have the following ready:
- Flutter SDK installed (latest stable version recommended)
- Android Studio or VS Code with the Flutter plugin
- A basic understanding of Dart,
StatefulWidget, and async/await - An internet connection so the app can fetch data
Adding the HTTP Plugin
The easiest way to add the plugin is to run this command in your project folder:
flutter pub add http
Or add it manually to your pubspec.yaml file under dependencies:
dependencies:
http: ^1.2.2
Then run flutter pub get.
Note on plugin versions: The version ^0.13.1 used in the original tutorial is out of date. The http package is now on version 1.x, and there have been API changes — most notably, http.get() now requires a Uri object rather than accepting a URL string. The code below uses the current API.
The Full Code
Here's a complete working example. It fetches users from JSONPlaceholder (a free fake API for testing) and displays them in a scrollable DataTable.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Table In Flutter',
home: MyAppTable(),
));
}
class MyAppTable extends StatefulWidget {
const MyAppTable({super.key});
@override
State<MyAppTable> createState() => _MyAppTableState();
}
class _MyAppTableState extends State<MyAppTable> {
List<User> _users = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
try {
final users = await getAllUsers();
if (!mounted) return;
setState(() {
_users = users;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Failed to load users: $e';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Table Tutorial'),
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Please wait...'),
],
),
);
}
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 60, color: Colors.red),
const SizedBox(height: 16),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_isLoading = true;
_error = null;
});
_loadUsers();
},
child: const Text('Retry'),
),
],
),
),
);
}
// Wrap the DataTable in a horizontal scroll so it doesn't overflow
// on narrow screens.
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
dividerThickness: 2,
headingRowColor: MaterialStateProperty.all(
Colors.blue.shade50,
),
columns: const [
DataColumn(label: Text('ID')),
DataColumn(label: Text('NAME')),
DataColumn(label: Text('USERNAME')),
DataColumn(label: Text('EMAIL')),
],
rows: _users
.map(
(user) => DataRow(
cells: [
DataCell(Text(user.id.toString())),
DataCell(Text(user.name)),
DataCell(Text(user.username)),
DataCell(Text(user.email)),
],
),
)
.toList(),
),
),
);
}
}
Future<List<User>> getAllUsers() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode != 200) {
throw Exception('HTTP ${response.statusCode}');
}
final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;
return jsonList
.map((json) => User.fromJson(json as Map<String, dynamic>))
.toList();
}
class User {
final int id;
final String name;
final String username;
final String email;
const User({
required this.id,
required this.name,
required this.username,
required this.email,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
username: json['username'] as String,
email: json['email'] as String? ?? '',
);
}
}
How the Code Works
Let's walk through the important parts:
1. Fetching Data in initState
We call _loadUsers() from initState() so the fetch starts as soon as the screen mounts. We don't call setState() inside initState() itself — that's an anti-pattern that causes a rebuild before the widget is fully built. Instead, the async method calls setState() once the data comes back.
2. Loading State
Three variables drive the UI: _isLoading, _error, and _users. During the fetch, _isLoading is true and we show a spinner. If the fetch fails, _error holds a message and we show an error screen with a retry button. Once data arrives, we render the table.
3. HTTP Request
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
In modern http, you pass a Uri object — not a string. We also check the response status code before parsing, so a 500 error from the server doesn't silently produce an empty list.
4. JSON Parsing
final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;
return jsonList.map((json) => User.fromJson(json)).toList();
We decode the response body into a list, then map each item through a factory constructor on the User class. This gives us type-safe User objects instead of raw maps.
5. The DataTable Widget
DataTable takes two lists: columns (one DataColumn per heading) and rows (one DataRow per record, each with a matching number of DataCells). The number of cells in each row must equal the number of columns.
6. Horizontal Scroll
On narrow phones, a table with four columns will overflow. Wrapping it in a horizontal SingleChildScrollView lets the user swipe left and right to see all columns. Inside, a vertical SingleChildScrollView handles long lists. Together they make the table usable on any screen size.
7. The mounted Check
if (!mounted) return;
After every async call, we check whether the widget is still mounted before calling setState(). If the user navigated away while the request was in flight, this prevents a "setState called after dispose" error.
Taking It Further
This tutorial covers the basics, but here are things you'd add for a real app:
- Sorting:
DataColumnhas anonSortcallback. Wire it up to sort the list by the tapped column. - Selection: Set
DataRow.selectedand provide anonSelectChangedcallback to let users select rows. - Pagination: For large datasets, use
PaginatedDataTableinstead ofDataTable. It handles the page controls for you. - RefreshIndicator: Wrap the table in a
RefreshIndicatorso users can pull down to reload. - Error handling: Catch
SocketExceptionseparately to show a specific "no internet" message instead of a generic error. - State management: For anything beyond a simple screen, move the fetch logic into a provider, bloc, or riverpod notifier so the widget doesn't handle async directly.
A word of caution on JSON parsing: the as int and as String casts will throw if the API returns unexpected types. In production, either use a code generator like json_serializable or check types before casting.
Wrapping Up
You now know how to fetch live data from a REST API and display it in a Flutter DataTable, with loading and error states handled cleanly. This is a pattern you'll reuse in dashboards, admin panels, and any app that shows tabular data.
The next natural step is to add sorting, filtering, and a search box. Once you're comfortable with the basics, check out PaginatedDataTable for larger datasets.
3 comments:
how to handle error. if we get null from server or api in response or some false message how can we display
check the response body in condition and basis of condition you can return the Widget object.
Sir Its not populting data on First time run..
After running i refresh then it display data
Post a Comment