Skip to content

Fetching Data with http

In React you reach for fetch or axios to make HTTP requests — they are either built into the browser or installed as a single package. Flutter has no built-in HTTP client, so the ecosystem settled on the http package from the Dart team. The mental model is the same: fire a request, await the response, inspect the status code, and decode the body. The differences are at the API surface.

Note: Live network calls may be blocked in the DartPad sandbox — run the real http example locally after adding http: ^1.2.0 to your pubspec.yaml dependencies.

React
// React — fetch (built-in) or axios
import axios from 'axios';
// Using fetch
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const data = await res.json();
return data;
}
// Using axios (throws automatically on non-2xx)
async function getUserAxios(id) {
const { data } = await axios.get(
`https://api.example.com/users/${id}`
);
return data;
}
Flutter
// Flutter — package:http
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<Map<String, dynamic>> getUser(int id) async {
final uri = Uri.parse(
'https://api.example.com/users/$id',
);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('HTTP error: ${response.statusCode}');
}
// response.body is a String — decode manually
final data = jsonDecode(response.body)
as Map<String, dynamic>;
return data;
}

The DartPad below skips the live network call and instead uses Future.delayed to simulate async latency — identical to how you would mock an API in a Jest test. The JSON decoding logic is real dart:convert code.

import 'dart:convert';
import 'package:flutter/material.dart';

// Simulates a network call — returns after 1 second
// In production replace this with http.get(uri)
Future<Map<String, dynamic>> fetchUser() {
  return Future.delayed(
    const Duration(seconds: 1),
    () => jsonDecode('{"id": 1, "name": "Ada Lovelace", "email": "[email protected]"}')
        as Map<String, dynamic>,
  );
}

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HTTP Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const UserScreen(),
    );
  }
}

class UserScreen extends StatelessWidget {
  const UserScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Fetch User'),
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
      ),
      body: FutureBuilder<Map<String, dynamic>>(
        future: fetchUser(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(
              child: Text(
                'Error: ${snapshot.error}',
                style: const TextStyle(color: Colors.red),
              ),
            );
          }
          final user = snapshot.data!;
          return Center(
            child: Card(
              margin: const EdgeInsets.all(24),
              elevation: 4,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              child: Padding(
                padding: const EdgeInsets.symmetric(
                  horizontal: 28,
                  vertical: 24,
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        CircleAvatar(
                          backgroundColor: Colors.indigo,
                          child: Text(
                            '${user['name'][0]}',
                            style: const TextStyle(color: Colors.white),
                          ),
                        ),
                        const SizedBox(width: 12),
                        Text(
                          user['name'] as String,
                          style: const TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 16),
                    _Row(label: 'ID', value: '${user['id']}'),
                    const SizedBox(height: 8),
                    _Row(label: 'Email', value: user['email'] as String),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class _Row extends StatelessWidget {
  final String label;
  final String value;
  const _Row({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(
          width: 56,
          child: Text(
            '$label:',
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              color: Colors.indigo,
            ),
          ),
        ),
        Expanded(child: Text(value)),
      ],
    );
  }
}
Why does the `http` package require an explicit status code check while `axios` does not?
What does `jsonDecode(response.body)` return by default when the JSON root is an object?
Which argument type does `http.get` accept instead of a plain string URL?