Here is the complete translation of the tutorial into English, preserving the structured code blocks, technical accuracy, and formatting.
Introduction: Why REST API Integration Matters in Your Flutter App?
Welcome to AnakInformatika! If you are a Flutter developer looking to build dynamic, interactive, and always up-to-date applications, mastering this Tutorial on Integrating REST APIs with HTTP / Dio in Flutter is an essential skill. Modern applications almost always rely on data coming from external servers, which is where REST APIs play a crucial role.
REST (Representational State Transfer) API allows our application to communicate with a server to retrieve data (GET), send new data (POST), update data (PUT/PATCH), or delete data (DELETE). In the Flutter ecosystem, there are two popular libraries that serve as the primary choices for handling these HTTP requests: the http package and Dio. Both have their own advantages and disadvantages, and this tutorial will cover both in-depth, providing you with a comprehensive understanding and production-ready code examples.
Let's dive into how to make your Flutter application come "alive" by interacting with the outside world!
Prerequisites
Before starting this API integration journey, make sure you meet the following requirements:
-
Flutter SDK: Installed and properly configured on your system.
-
IDE: Visual Studio Code or Android Studio with the Flutter plugin installed.
-
Basic Knowledge of Dart & Flutter: Familiarity with basic concepts such as Widgets, State, StatelessWidget/StatefulWidget, and Futures.
-
Internet Connection: Required to download packages and access the API.
Flutter Project Setup
First, let's create a new Flutter project and add the necessary dependencies. Open your terminal or command prompt and run the following commands:
flutter create rest_api_integration_demo
cd rest_api_integration_demo
Next, open your pubspec.yaml file and add the http and dio packages under the dependencies: section. We will use JSONPlaceholder as a free, public REST API for this demo.
dependencies:
flutter:
sdk: flutter
http: ^1.2.1 # Latest version at the time of writing
dio: ^5.4.3+1 # Latest version at the time of writing
# Add other packages if needed
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
Save the pubspec.yaml file and run flutter pub get to download the packages.
Creating the Data Model (Serialization)
To manage the data received from the API (which is typically in JSON format), it is highly recommended to create a data model (Dart class). This helps in deserializing JSON into Dart objects and vice versa (serialization). We will use the "Posts" data from JSONPlaceholder.
Create a models folder inside the lib directory, then create a file named post.dart:
// lib/models/post.dart
import 'dart:convert';
class Post {
final int? userId;
final int? id;
final String title;
final String body;
Post({
this.userId,
this.id,
required this.title,
required this.body,
});
// Factory constructor to create a Post instance from JSON (deserialization)
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'] as int?,
id: json['id'] as int?,
title: json['title'] as String,
body: json['body'] as String,
);
}
// Method to convert a Post instance into JSON (serialization)
Map<String, dynamic> toJson() {
return {
'userId': userId,
'id': id,
'title': title,
'body': body,
};
}
}
Model Code Explanation:
-
Post.fromJson(Map<String, dynamic> json): This is a factory constructor used to convert JSON data (in the form of aMap<String, dynamic>) into aPostobject. This is essential when you receive a response from the API. -
toJson(): This method does the opposite—it converts aPostobject into aMap<String, dynamic>, which can then be encoded into a JSON string to be sent to the API (e.g., for POST or PUT requests).
Integrating REST API with the HTTP Package
The http package is Dart's built-in, lightweight, and easy-to-use HTTP client. It is perfect for simple projects or when you do not require advanced features like interceptors or caching available in Dio.
Creating the API Service with HTTP
Create a services folder inside lib, then create a file named post_service_http.dart:
// lib/services/post_service_http.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:rest_api_integration_demo/models/post.dart';
class PostServiceHttp {
final String _baseUrl = 'https://jsonplaceholder.typicode.com';
// --- GET Request ---
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('$_baseUrl/posts'));
if (response.statusCode == 200) {
// If the server returns an OK response (status code 200), parse the JSON.
List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Post.fromJson(json)).toList();
} else {
// If the server does not return an OK response, throw an exception.
throw Exception('Failed to load posts: ${response.statusCode}');
}
}
// --- POST Request ---
Future<Post> createPost(Post post) async {
final response = await http.post(
Uri.parse('$_baseUrl/posts'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(post.toJson()), // Converts Post object to JSON string
);
if (response.statusCode == 201) { // 201 Created
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create post: ${response.statusCode}');
}
}
// --- PUT/PATCH Request (for updating) ---
Future<Post> updatePost(int id, Post post) async {
final response = await http.put(
Uri.parse('$_baseUrl/posts/$id'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(post.toJson()),
);
if (response.statusCode == 200) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update post: ${response.statusCode}');
}
}
// --- DELETE Request ---
Future<void> deletePost(int id) async {
final response = await http.delete(
Uri.parse('$_baseUrl/posts/$id'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
print('Post with ID $id deleted successfully.');
} else {
throw Exception('Failed to delete post: ${response.statusCode}');
}
}
}
HTTP Service Code Explanation:
-
_baseUrl: Defines the base URL of the API for easier management. -
http.get(Uri.parse('$_baseUrl/posts')): Sends a GET request to the/postsendpoint.Uri.parse()converts the URL string into aUriobject required by HTTP methods. -
jsonDecode(response.body): Converts the JSON string received from the server into a Dart object (usually aList<dynamic>orMap<String, dynamic>). -
data.map((json) => Post.fromJson(json)).toList(): Iterates through each JSON item in the list and converts it into aPostobject using thePost.fromJson()factory constructor. -
response.statusCode == 200: Checks if the request was successful. A status code of 200 means OK, while 201 means Created (used for POST). -
http.post(...),http.put(...),http.delete(...): Similar to GET, but utilizing different HTTP methods. -
headers: {'Content-Type': 'application/json; charset=UTF-8'}: Crucial for telling the server that we are sending data in JSON format. -
body: jsonEncode(post.toJson()): Converts thePostobject into a JSON string before sending it to the server.
Using the HTTP Service in the UI
Now, let's use this service inside our application's UI. Modify the main.dart file:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:rest_api_integration_demo/models/post.dart';
import 'package:rest_api_integration_demo/services/post_service_http.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter REST API HTTP Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const PostListPageHttp(),
);
}
}
class PostListPageHttp extends StatefulWidget {
const PostListPageHttp({super.key});
@override
State<PostListPageHttp> createState() => _PostListPageHttpState();
}
class _PostListPageHttpState extends State<PostListPageHttp> {
late Future<List<Post>> _futurePosts;
final PostServiceHttp _postService = PostServiceHttp();
@override
void initState() {
super.initState();
_futurePosts = _postService.fetchPosts(); // Call API during initState
}
// Example function to create a new post
Future<void> _createPost() async {
final newPost = Post(title: 'New Title From HTTP', body: 'New post content from Flutter app using HTTP!');
try {
final createdPost = await _postService.createPost(newPost);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Post created successfully: ${createdPost.title}')),
);
setState(() {
_futurePosts = _postService.fetchPosts(); // Refresh list
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to create post: $e')),
);
}
}
// Example function to delete a post
Future<void> _deletePost(int id) async {
try {
await _postService.deletePost(id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Post ID $id deleted successfully.')),
);
setState(() {
_futurePosts = _postService.fetchPosts(); // Refresh list
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete post: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Posts (HTTP Package)'),
),
body: FutureBuilder<List<Post>>(
future: _futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final post = snapshot.data![index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(post.title),
subtitle: Text(post.body),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deletePost(post.id!), // Ensure ID is not null
),
onTap: () {
// Example: Navigate to detail or update post
print('Post ${post.id} selected');
},
),
);
},
);
} else {
return const Center(child: Text('No post data available.'));
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: _createPost,
child: const Icon(Icons.add),
),
);
}
}
HTTP UI Code Explanation:
-
FutureBuilder<List<Post>>: A highly useful widget for handling asynchronous operations. It builds the UI based on the latest snapshot of the provided Future (_futurePosts). -
ConnectionState.waiting: Displays a loading indicator while data is being fetched. -
snapshot.hasError: Displays an error message if something goes wrong. -
snapshot.hasData: Displays the data if successfully fetched, using aListView.builderin this case. -
initState(): The ideal place to trigger the API call for the first time when the widget is initialized. -
_createPost()and_deletePost(): Examples of triggering POST and DELETE requests from the UI. After a successful operation,setStateis called to refresh the data by refetching_postService.fetchPosts().
Integrating REST API with the Dio Package
Dio is a more advanced and feature-rich HTTP client. It offers interceptors, form data, request cancellation, and much more. It is an excellent choice for larger and more complex applications.
Creating the API Service with Dio
Create a file named post_service_dio.dart inside the lib/services folder:
// lib/services/post_service_dio.dart
import 'package:dio/dio.dart';
import 'package:rest_api_integration_demo/models/post.dart';
class PostServiceDio {
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 5), // 5 seconds
receiveTimeout: const Duration(seconds: 3), // 3 seconds
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
));
// --- GET Request ---
Future<List<Post>> fetchPosts() async {
try {
final response = await _dio.get('/posts');
if (response.statusCode == 200) {
List<dynamic> data = response.data;
return data.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts: ${response.statusCode}');
}
} on DioException catch (e) { // Handle DioException specifically
throw Exception('Failed to load posts: ${e.message}');
} catch (e) {
throw Exception('An unexpected error occurred: $e');
}
}
// --- POST Request ---
Future<Post> createPost(Post post) async {
try {
final response = await _dio.post(
'/posts',
data: post.toJson(), // Dio automatically handles JSON encoding
);
if (response.statusCode == 201) {
return Post.fromJson(response.data);
} else {
throw Exception('Failed to create post: ${response.statusCode}');
}
} on DioException catch (e) {
throw Exception('Failed to create post: ${e.message}');
}
}
// --- PUT/PATCH Request (for updating) ---
Future<Post> updatePost(int id, Post post) async {
try {
final response = await _dio.put(
'/posts/$id',
data: post.toJson(),
);
if (response.statusCode == 200) {
return Post.fromJson(response.data);
} else {
throw Exception('Failed to update post: ${response.statusCode}');
}
} on DioException catch (e) {
throw Exception('Failed to update post: ${e.message}');
}
}
// --- DELETE Request ---
Future<void> deletePost(int id) async {
try {
final response = await _dio.delete(
'/posts/$id',
);
if (response.statusCode == 200) {
print('Post with ID $id deleted successfully.');
} else {
throw Exception('Failed to delete post: ${response.statusCode}');
}
} on DioException catch (e) {
throw Exception('Failed to delete post: ${e.message}');
}
}
// Example of using Interceptors (Optional)
void addInterceptors() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Do something before the request is sent
print('REQUEST[${options.method}] => PATH: ${options.path}');
return handler.next(options); // Continue request
},
onResponse: (response, handler) {
// Do something with response data
print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
return handler.next(response); // Continue response
},
onError: (DioException e, handler) {
// Do something with DioError
print('ERROR[${e.response?.statusCode}] => PATH: ${e.requestOptions.path}');
return handler.next(e); // Continue error
},
),
);
}
}
Dio Service Code Explanation:
-
final Dio _dio = Dio(...): Instantiates Dio. Here, we can globally configureBaseOptionssuch asbaseUrl,connectTimeout,receiveTimeout, andheadersfor all requests. -
_dio.get('/posts'): Since thebaseUrlis already configured, we only need to provide the relative path. -
response.data: Dio automatically decodes JSON responses into Dart objects (MaporList), so callingjsonDecode()manually is completely unnecessary. -
data: post.toJson(): For POST/PUT requests, Dio automatically encodes the provided Dart map into a JSON string, eliminating the need for manualjsonEncode(). -
on DioException catch (e): Dio provides a specialized error type calledDioException, which gives detailed information about the issue, such as error types (connection, timeout, etc.) and server responses. -
addInterceptors(): An example of adding interceptors. Interceptors allow you to intercept and modify requests or responses before they are processed. This is highly beneficial for logging, handling authentication tokens, or global error handling.
Using the Dio Service in the UI
Let's create a new file for the Dio demo so it does not get mixed up with HTTP. Create a file named post_list_page_dio.dart inside the lib/ folder:
// lib/post_list_page_dio.dart
import 'package:flutter/material.dart';
import 'package:rest_api_integration_demo/models/post.dart';
import 'package:rest_api_integration_demo/services/post_service_dio.dart';
class PostListPageDio extends StatefulWidget {
const PostListPageDio({super.key});
@override
State<PostListPageDio> createState() => _PostListPageDioState();
}
class _PostListPageDioState extends State<PostListPageDio> {
late Future<List<Post>> _futurePosts;
final PostServiceDio _postService = PostServiceDio();
@override
void initState() {
super.initState();
_postService.addInterceptors(); // Enable interceptors (optional)
_futurePosts = _postService.fetchPosts();
}
Future<void> _createPost() async {
final newPost = Post(title: 'New Title From Dio', body: 'New post content from Flutter app using Dio!');
try {
final createdPost = await _postService.createPost(newPost);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Post created successfully: ${createdPost.title}')),
);
setState(() {
_futurePosts = _postService.fetchPosts();
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to create post: $e')),
);
}
}
Future<void> _deletePost(int id) async {
try {
await _postService.deletePost(id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Post ID $id deleted successfully.')),
);
setState(() {
_futurePosts = _postService.fetchPosts();
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete post: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Posts (Dio Package)'),
),
body: FutureBuilder<List<Post>>(
future: _futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final post = snapshot.data![index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(post.title),
subtitle: Text(post.body),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deletePost(post.id!),
),
onTap: () {
print('Post ${post.id} selected');
},
),
);
},
);
} else {
return const Center(child: Text('No post data available.'));
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: _createPost,
child: const Icon(Icons.add),
),
);
}
}
To display the Dio page, you can swap the home property in main.dart:
// lib/main.dart (Inside MyApp component)
// ...
home: const PostListPageDio(), // Change to this to see the Dio demo
// ...
The UI structure for Dio is almost identical to HTTP because both return a Future<List<Post>>. The primary difference lies inside the implementation details at the service layer.
Practical Tips and Best Practices
-
Separation of Concerns:
-
Model: Exclusively for defining data structures and handling JSON serialization/deserialization.
-
Service/Repository: Manages API call logic, error handling, and converting responses into Dart objects. This keeps your UI clean and detached from raw API logic.
-
UI (Widgets): Focuses solely on data presentation and user interaction by calling methods from the service.
-
-
Robust Error Handling: Always wrap your API calls in
try-catchblocks. Check thestatusCodefrom HTTP responses. When using Dio, handleDioExceptionto catch more specific errors and provide clean feedback (such as a SnackBar or dialog) to the user. -
State Management: Use
FutureBuilderto display asynchronous data gracefully. For complex applications, consider full state management solutions like Provider, Riverpod, BLoC, or GetX to handle loading, error, and data states efficiently. -
Timeouts: Always configure timeouts for your API requests (
connectTimeout,receiveTimeout). This prevents your app from being stuck infinitely waiting for a server response. Dio simplifies this throughBaseOptions. -
Interceptors (Dio Specific): Take advantage of interceptors for:
-
Logging: Outputting every request and response for easy debugging.
-
Authentication: Automatically attaching authentication tokens (e.g., Bearer Tokens) to every outgoing header.
-
Global Error Handling: Handling specific types of server errors centrally in one place.
-
-
Base URL Management: Always isolate your base URL from specific endpoints. This makes switching environments (development, staging, production) incredibly easy.
-
Security: Never hardcode sensitive API keys directly into your source code. Use environment variables or configuration management solutions. Always use HTTPS for API communication.
-
Testability: By extracting your API logic into a separate service, you can write isolated unit tests for your services much more easily.
Conclusion
Congratulations! You have successfully completed this Tutorial on Integrating REST APIs with HTTP / Dio in Flutter. You now possess a solid understanding of how to fetch, create, update, and delete data using the two most popular HTTP libraries in Flutter: http and Dio.
Both http and Dio are incredibly powerful tools, and choosing between them usually depends on the specific scale of your project. For simple applications, http gets the job done efficiently. For larger applications that demand advanced capabilities like interceptors, global timeouts, and richer error metrics, Dio stands out as the superior choice.
Keep practicing, experiment with different APIs, and feel free to revisit this tutorial whenever you need a reference guide. Integrating REST APIs is one of the most fundamental skills that will take your Flutter development to the next level. See you in the next AnakInformatika tutorial!