Skip to content

Flutter - Accessing REST API

Modern mobile applications frequently interact with backend services through REST APIs to fetch or send data. Flutter provides robust tools for making HTTP requests.

The core package for basic HTTP requests is package:http. It’s a Future-based library leveraging Dart’s async and await for handling asynchronous network operations.

Key features and methods:

  • Top-level Functions: Provides simple functions for common requests:
    • http.get(url, headers: ...): Sends a GET request.
    • http.post(url, headers: ..., body: ..., encoding: ...): Sends a POST request, often with a request body (e.g., JSON).
    • http.put(url, headers: ..., body: ...): Sends a PUT request.
    • http.patch(url, headers: ..., body: ...): Sends a PATCH request.
    • http.delete(url, headers: ...): Sends a DELETE request.
    • http.read(url, headers: ...): Simple GET request that returns the response body as a String.
  • Response Object: Most methods return a Future<Response>. The Response object contains:
    • statusCode: The HTTP status code (e.g., 200 for OK, 404 for Not Found).
    • body: The response body as a String.
    • headers: A map of response headers.
    • contentLength: The length of the response body.
  • JSON Handling: You typically need dart:convert to encode Dart objects (like Maps) into JSON strings for request bodies and decode JSON strings from response bodies into Dart objects.
  • Error Handling: Network errors or non-2xx status codes should be handled using try-catch blocks and checking the response.statusCode.
import 'dart:convert'; // For jsonDecode, jsonEncode
import 'package:http/http.dart' as http;
Future<void> fetchData() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1'); // Use Uri.parse
try {
final response = await http.get(url);
if (response.statusCode == 200) {
// Success!
final data = jsonDecode(response.body) as Map<String, dynamic>;
print('Title: ${data['title']}');
} else {
// Server returned an error status code
print('Request failed with status: ${response.statusCode}.');
}
} catch (e) {
// Handle network errors or other exceptions
print('An error occurred: $e');
}
}

While http is great for basic needs, the dio package is a popular alternative offering more advanced features like interceptors (for logging, authentication), global configuration, FormData support, request cancellation, and more streamlined error handling. Consider dio for more complex API interactions.

Example: Fetching Products from a REST API

Section titled “Example: Fetching Products from a REST API”

Let’s adapt the product app to fetch data from a simple JSON REST endpoint.

  1. Create Project: flutter create product_rest_app.
  2. Add http Dependency: Add http: ^1.1.0 (or latest) to pubspec.yaml and run flutter pub get.
  3. Add Assets: (Optional, if using local images) Create assets/images/, add images, declare in pubspec.yaml.
  4. Prepare JSON Data: Create a products.json file with your product data:
[
{
"id": 1,
"name": "iPhone",
"description": "The stylish phone ever",
"price": 1000,
"image": "iphone.png"
},
{
"id": 2,
"name": "Pixel",
"description": "The most feature phone ever",
"price": 800,
"image": "pixel.png"
},
{
"id": 3,
"name": "Laptop",
"description": "Most productive development tool",
"price": 2000,
"image": "laptop.png"
}
// ... more products
]
  1. Set up a Simple Web Server: You need a way to serve this products.json file over HTTP.
    • Node.js http-server:
  2. * Install Node.js if you haven't.
  3. * Install the server: `npm install -g http-server`
  4. * Navigate to the directory containing `products.json` in your terminal.
  5. * Run: `http-server . -p 8000 --cors` (The `--cors` flag is important for web testing). Note the IP address provided (e.g., `http://192.168.1.10:8000`).
    • Python:
  6. * Navigate to the directory containing `products.json`.
  7. * Run: `python -m http.server 8000` (Python 3). Use `127.0.0.1:8000` or your machine's local network IP.
    • Other: Use any simple web server (Apache, Nginx, VS Code Live Server extension). Make sure it’s accessible from your emulator/device.
  8. Define Product Model (lib/product.dart): Include a factory constructor to parse JSON.
class Product {
final int id;
final String name;
final String description;
final int price;
final String image;
const Product({
required this.id,
required this.name,
required this.description,
required this.price,
required this.image,
});
// Factory constructor to create a Product from a JSON map
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
description: json['description'] as String,
price: json['price'] as int,
image: json['image'] as String,
);
}
}
  1. Create API Service (lib/api_service.dart): Encapsulate the fetching logic.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'product.dart'; // Assuming product.dart is in lib/
class ApiService {
// Replace with your actual server URL
// For Android emulator accessing localhost: use 10.0.2.2
// For physical device/iOS simulator: use your machine's local network IP
static const String _baseUrl = 'http://10.0.2.2:8000'; // Example for Android emulator
// static const String _baseUrl = 'http://192.168.1.10:8000'; // Example for local network IP
Future<List<Product>> fetchProducts() async {
final url = Uri.parse('$_baseUrl/products.json');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
// If the server returns an OK response, parse the JSON.
List<dynamic> body = jsonDecode(response.body);
List<Product> products = body
.map((dynamic item) => Product.fromJson(item as Map<String, dynamic>))
.toList();
return products;
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load products (Status code: ${response.statusCode})');
}
} catch (e) {
// Handle network errors or parsing errors
print('Error fetching products: $e');
throw Exception('Failed to load products: $e');
}
}
// Add methods for POST, PUT, DELETE etc. as needed
}
  1. Update UI (lib/main.dart): Use FutureBuilder to handle the asynchronous data fetching.
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'product.dart';
// Import ProductBox, ProductPage, RatingBox widgets from previous examples if needed
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Product REST App',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ProductListPage(),
);
}
}
class ProductListPage extends StatefulWidget {
const ProductListPage({Key? key}) : super(key: key);
@override
_ProductListPageState createState() => _ProductListPageState();
}
class _ProductListPageState extends State<ProductListPage> {
late Future<List<Product>> _productsFuture;
final ApiService _apiService = ApiService();
@override
void initState() {
super.initState();
_productsFuture = _apiService.fetchProducts();
}
void _refreshProducts() {
setState(() {
_productsFuture = _apiService.fetchProducts();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products from API'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _refreshProducts, // Add a refresh button
tooltip: 'Refresh',
)
],
),
body: Center(
child: FutureBuilder<List<Product>>(
future: _productsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// While data is loading
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
// If an error occurred
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${snapshot.error}'),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _refreshProducts,
child: const Text('Retry'),
)
],
);
} else if (snapshot.hasData) {
// If data is successfully loaded
final products = snapshot.data!;
if (products.isEmpty) {
return const Text('No products found.');
}
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
// Replace with your actual ProductBox or ListTile widget
return ListTile(
// leading: Image.network('$_baseUrl/images/${product.image}'), // Example if serving images
leading: Image.asset('assets/images/${product.image}', width: 50, errorBuilder: (c, e, s) => Icon(Icons.image, size: 50)), // Example using local assets
title: Text(product.name),
subtitle: Text(product.description),
trailing: Text('$${product.price}'),
onTap: () {
// Navigate to detail page if needed
// Navigator.push(context, MaterialPageRoute(builder: (context) => ProductPage(item: product)));
},
);
},
);
} else {
// Should not happen in typical FutureBuilder usage with a non-null future
return const Text('No data available.');
}
},
),
),
);
}
}

Run the app. Ensure your simple web server is running and accessible. The app will show a loading indicator, then display the list of products fetched from products.json, or an error message if the request fails.