Flutter Live Data with DataTable Widget

Flutter DataTable with Live API Data: Complete Tutorial (2026) | D.Tech Academic

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
Flutter app showing a loading spinner before the DataTable appears
Loading state while the API call is in progress
Flutter DataTable widget displaying live user data from a REST API
DataTable rendering live API 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: DataColumn has an onSort callback. Wire it up to sort the list by the tapped column.
  • Selection: Set DataRow.selected and provide an onSelectChanged callback to let users select rows.
  • Pagination: For large datasets, use PaginatedDataTable instead of DataTable. It handles the page controls for you.
  • RefreshIndicator: Wrap the table in a RefreshIndicator so users can pull down to reload.
  • Error handling: Catch SocketException separately 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:

Unknown said...

how to handle error. if we get null from server or api in response or some false message how can we display

Deep Singh said...

check the response body in condition and basis of condition you can return the Widget object.

Osama said...

Sir Its not populting data on First time run..
After running i refresh then it display data