Skip to content

flutter_accessing_rest_apis

Most modern mobile applications need to interact with backend services to fetch or send data. REST (Representational State Transfer) APIs using JSON (JavaScript Object Notation) over HTTP/S are a common standard for this communication.

Flutter provides packages and built-in Dart features to easily consume REST APIs.

The core package for making HTTP requests in Dart/Flutter is http. It’s a Future-based library leveraging Dart’s async/await capabilities.

  1. Add the http package dependency to your pubspec.yaml:

  2. Run flutter pub get.

  3. Import the package in your Dart file, often using an alias:

The http package provides top-level functions for common HTTP methods:

  • http.get(Uri url, {Map<String, String>? headers}): Sends a GET request. Returns Future<http.Response>.
  • http.post(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}): Sends a POST request. body is often a JSON-encoded string. Returns Future<http.Response>.
  • http.put(...): Sends a PUT request.
  • http.patch(...): Sends a PATCH request.
  • http.delete(...): Sends a DELETE request.
  • http.head(...): Sends a HEAD request.

All these methods return a Future<http.Response>. The http.Response object contains:

  • statusCode: The HTTP status code (e.g., 200 for OK, 404 for Not Found, 500 for Server Error).
  • body: The response body as a String.
  • headers: A map of response headers.
  • contentLength: The length of the response body in bytes.
  • request: Information about the original request.

Let’s fetch a list of sample ‘todos’ from the JSONPlaceholder API.

Create a simple class to represent the data structure (e.g., lib/todo.dart). Adding a fromJson factory constructor simplifies parsing.

class Todo {
final int userId;
final int id;
final String title;
final bool completed;
const Todo({
required this.userId,
required this.id,
required this.title,
required this.completed,
});
// Factory constructor to create a Todo from JSON
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
completed: json['completed'] as bool,
);
}
}

Create a function to handle the API call and parsing.

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'todo.dart'; // Import your Todo model
Future<List<Todo>> fetchTodos() async {
// Define the API endpoint URL
final url = Uri.parse('https://jsonplaceholder.typicode.com/todos');
try {
// Make the GET request
final response = await http.get(url);
// Check if the request was successful (status code 200)
if (response.statusCode == 200) {
// Decode the JSON response body (which is a String)
// The result is a List<dynamic> because jsonDecode doesn't know the specific types
List<dynamic> jsonResponse = jsonDecode(response.body);
// Map the dynamic list to a List<Todo> using the fromJson factory
return jsonResponse.map((data) => Todo.fromJson(data)).toList();
} else {
// If the server did not return a 200 OK response,
// throw an exception.
throw Exception('Failed to load todos. Status code: ${response.statusCode}');
}
} catch (e) {
// Handle potential network errors or JSON parsing errors
print('Error fetching todos: $e');
throw Exception('Failed to load todos: $e');
}
}

Use FutureBuilder to handle the asynchronous nature of the API call and display the data or loading/error states.

import 'package:flutter/material.dart';
// Import fetchTodos function and Todo model
import 'api_service.dart';
import 'todo.dart';
class TodoListScreen extends StatefulWidget {
const TodoListScreen({Key? key}) : super(key: key);
@override
_TodoListScreenState createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
late Future<List<Todo>> futureTodos;
@override
void initState() {
super.initState();
// Call the fetch function when the widget is initialized
futureTodos = fetchTodos();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Fetched Todos'),
),
body: Center(
child: FutureBuilder<List<Todo>>(
future: futureTodos, // The Future<List<Todo>> to observe
builder: (context, snapshot) {
// Check the connection state
if (snapshot.connectionState == ConnectionState.waiting) {
// If waiting, show a loading indicator
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
// If there's an error, display it
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
// If data is available, display the list
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: CircleAvatar(child: Text(todo.id.toString())),
title: Text(todo.title),
trailing: Icon(
todo.completed ? Icons.check_box : Icons.check_box_outline_blank,
color: todo.completed ? Colors.green : Colors.grey,
),
);
},
);
} else {
// If no data and no error (shouldn't happen with this API if successful)
return const Text('No todos found.');
}
},
),
),
);
}
}
// --- Usage Example ---
// void main() => runApp(MaterialApp(home: TodoListScreen()));

To send data (e.g., creating a new resource), use http.post.

Future<http.Response> createTodo(String title) async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/todos');
try {
final response = await http.post(
url,
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', // Set content type
},
// Encode the data to JSON format
body: jsonEncode(<String, dynamic>{
'title': title,
'userId': 1, // Example data
'completed': false,
}),
);
if (response.statusCode == 201) { // 201 Created is common for successful POST
print('Todo created successfully: ${response.body}');
return response;
} else {
throw Exception('Failed to create todo. Status code: ${response.statusCode}');
}
} catch (e) {
print('Error creating todo: $e');
throw Exception('Failed to create todo: $e');
}
}
  • Always check response.statusCode: Don’t assume a request was successful. Check for 2xx status codes for success, and handle 4xx (client errors) and 5xx (server errors) appropriately.
  • Use try-catch: Network requests can fail for various reasons (no internet, DNS issues, timeouts). Wrap your http calls in try-catch blocks.
  • Handle JSON Decoding Errors: jsonDecode can throw a FormatException if the response body is not valid JSON. Include this in your error handling.
  • Use Data Models: Parsing JSON into strongly-typed Dart objects makes your code safer and easier to work with. Consider packages like json_serializable to automate fromJson/toJson generation for complex models.
  • Provide User Feedback: Show loading indicators (CircularProgressIndicator) while fetching data and display clear error messages to the user.
  • Consider http.Client: For making multiple requests to the same server, creating an http.Client instance can be more efficient as it can reuse TCP connections. Remember to client.close() when done.
  • Alternative Packages: For more advanced features like interceptors, FormData support, cancellation, and better error handling, consider packages like dio.

Accessing REST APIs is a core part of many Flutter apps. The http package provides a solid foundation, and using it with FutureBuilder and proper error handling allows you to integrate network data effectively into your UI.