How to make network requests in Flutter

Using the http package, Dio, and parsing JSON with code generation in Flutter

, updated

Fetching data from APIs is essential for most Flutter apps. Here’s how to do it well.

Using the http package

The simplest approach for basic REST calls:

dependencies:
  http: ^1.2.0
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<List<User>> fetchUsers() async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));

  if (response.statusCode == 200) {
    final List data = json.decode(response.body);
    return data.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load users: ${response.statusCode}');
  }
}

// POST request
Future<User> createUser(String name) async {
  final response = await http.post(
    Uri.parse('https://api.example.com/users'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({'name': name}),
  );

  if (response.statusCode == 201) {
    return User.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to create user: ${response.statusCode}');
  }
}

Using Dio — advanced HTTP client

Dio supports interceptors, form data, request cancellation, and timeout configuration:

dependencies:
  dio: ^5.4.0
import 'package:dio/dio.dart';

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: Duration(seconds: 10),
  receiveTimeout: Duration(seconds: 10),
  headers: {'Accept': 'application/json'},
));

// GET
Future<List<User>> fetchUsers() async {
  final response = await dio.get('/users');
  return (response.data as List).map((json) => User.fromJson(json)).toList();
}

// POST
Future<User> createUser(String name) async {
  final response = await dio.post('/users', data: {'name': name});
  return User.fromJson(response.data);
}

// Interceptors — add auth tokens, logging
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    options.headers['Authorization'] = 'Bearer ${AuthToken.get()}';
    return handler.next(options);
  },
  onError: (error, handler) {
    if (error.response?.statusCode == 401) {
      // Refresh token and retry
    }
    return handler.next(error);
  },
));

JSON serialization with code generation

For production apps, use json_serializable instead of hand-written fromJson/toJson:

dependencies:
  json_annotation: ^4.9.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.8.0

Define your model:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String? email;

  User({required this.id, required this.name, this.email});

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Run the generator:

dart run build_runner build --delete-conflicting-outputs

Displaying network data

Use FutureBuilder to show data with loading and error states:

class UserList extends StatelessWidget {
  final Future<List<User>> users;

  const UserList({required this.users, super.key});

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<User>>(
      future: users,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        } else if (snapshot.hasError) {
          return Center(child: Text('Error: ${snapshot.error}'));
        } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
          return Center(child: Text('No users found'));
        }

        final items = snapshot.data!;
        return ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            final user = items[index];
            return ListTile(title: Text(user.name), subtitle: Text(user.email ?? ''));
          },
        );
      },
    );
  }
}