Advanced Integration of RESTful APIs in Flutter Applications

Yawar Osman
4 min readJan 6, 2024

--

Integrating RESTful APIs into Flutter applications is essential for dynamic content and server-side functionality. This article revisits and enhances the previous guide, focusing on advanced practices including serialization on a separate thread for increased performance.

Understanding RESTful APIs in Flutter

RESTful APIs facilitate communication between Flutter applications and servers. They operate over standard HTTP methods like GET, POST, PUT, and DELETE.

Setting Up the Flutter Environment

Ensure your Flutter environment is correctly set up and add the http package to your pubspec.yaml:

dependencies:
flutter:
sdk: flutter
http: ^0.13.3

Run flutter pub get to install the package.

Making Network Requests

Utilize the http package for HTTP requests. Here's a refined approach:

Improved GET Request Example

import 'package:http/http.dart' as http;

Future<void> fetchUserData() async {
try {
var response = await http.get(Uri.parse('https://example.com/users'))
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body);
// Further processing
} else {
// Error handling
}
} catch (e) {
// Exception handling (timeouts, no internet, etc.)
}
}

Handling JSON Data

JSON Serialization on a Separate Thread

Serializing JSON on a separate thread can significantly enhance performance, especially for complex or large JSON data. Flutter can achieve this using compute function which runs expensive tasks in a background isolate, preventing UI jank.

import 'dart:convert';
import 'package:flutter/foundation.dart';
Future<User> fetchUserData() async {
var response = await http.get(Uri.parse('https://example.com/users'));
return compute(parseUserData, response.body);
}
// This function is called in a background isolate
User parseUserData(String responseBody) {
var jsonData = jsonDecode(responseBody) as Map<String, dynamic>;
return User.fromJson(jsonData);
}

This approach is beneficial for preventing UI freezes and improving app responsiveness.

Converting JSON to Dart Objects

class User {
final String name;
final String email;
User({required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'],
email: json['email'],
);
}
}

Handling Asynchronous Operations

Using async-await with Error and Exception Handling

Handle asynchronous operations using async-await and include comprehensive error and exception handling.

Future<void> fetchUserData() async {
try {
var response = await http.get(Uri.parse('https://example.com/users'));
if (response.statusCode == 200) {
var user = compute(parseUserData, response.body);
// Use the user object
} else {
// Error handling
}
} catch (e) {
// Exception handling
}
}

Advanced Best Practices

Error Handling

Robust error handling is essential in network requests to manage unexpected scenarios gracefully. In Flutter, this involves catching exceptions that might occur during the request, such as connectivity issues, server errors, or invalid responses.

Example of error handling in a network request:

Future<void> fetchUserData() async {
try {
var response = await http.get(Uri.parse('https://example.com/users'));
if (response.statusCode == 200) {
// Process response
} else {
// Handle server errors
}
} on SocketException {
// Handle network issues
} on HttpException {
// Handle HTTP protocol errors
} on FormatException {
// Handle data format errors
} catch (e) {
// Handle any other exceptions
}
}

Timeouts

Timeouts are crucial to prevent your application from hanging due to prolonged network requests. Implementing timeouts ensures that the request either completes within a specified duration or fails gracefully.

Example of setting a timeout for a network request:

Future<void> fetchUserData() async {
try {
var response = await http
.get(Uri.parse('https://example.com/users'))
.timeout(const Duration(seconds: 10));
// Process response
} on TimeoutException {
// Handle timeout
} catch (e) {
// Handle other exceptions
}
}

Cancellation

Request cancellation is necessary to handle scenarios where a user might navigate away from the page or cancel the operation. The http package doesn't provide direct support for request cancellation, but it can be implemented using StreamSubscription.

Example of request cancellation:

StreamSubscription? _subscription;

void fetchUserData() {
final request = Stream.fromFuture(http.get(Uri.parse('https://example.com/users')));
_subscription = request.listen(
(response) {
// Process response
},
onError: (e) {
// Handle error
},
onDone: () {
// Handle completion
},
cancelOnError: true,
);
}
void cancelRequest() {
_subscription?.cancel();
}

State Management

Efficient handling of network states is critical for maintaining a responsive UI. State management solutions like Provider or BLoC can help manage the state of your network requests and update the UI accordingly.

Example using Provider for state management:

class UserDataProvider with ChangeNotifier {
User? _user;
Future<void> fetchUserData() async {
try {
var response = await http.get(Uri.parse('https://example.com/users'));
if (response.statusCode == 200) {
_user = User.fromJson(jsonDecode(response.body));
notifyListeners();
}
} catch (e) {
// Handle errors
}
}
User? get user => _user;
}

Testing

Testing the network layer of your Flutter application is vital for ensuring reliability and robustness. Write unit tests to simulate different network scenarios and responses.

Example of a simple network test:

void main() {
group('User Data Fetch', () {
test('returns User if the http call completes successfully', () async {
final client = MockClient();

when(client.get(Uri.parse('https://example.com/users')))
.thenAnswer((_) async => http.Response('{"name": "John", "email": "john@example.com"}', 200));
expect(await fetchUserData(client), isA<User>());
});
});
}

Background Serialization

Serializing JSON in a background thread can significantly enhance app performance. This can be achieved using Flutter’s compute function, which executes expensive functions in a background isolate.

Example of background serialization:

import 'package:flutter/foundation.dart';

Future<User> fetchUserData(http.Client client) async {
final response = await client.get(Uri.parse('https://example.com/users'));
return compute(parseUser, response.body);
}
// This function runs in a background isolate
User parseUser(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<User>((json) => User.fromJson(json)).toList();
}

By running JSON parsing in the background, you prevent UI lag, especially with large or complex datasets.

yawarosman.com

--

--

Yawar Osman
Yawar Osman

Written by Yawar Osman

Project Manager || Software Developer || Team Leader || Flutter Developer

No responses yet