Hey there, Flutter developer! ๐ Ready to make your apps talk to the internet like a boss? Network handling is one of the most important skills you'll need as a mobile developer. Whether you're building a social media app, an e-commerce platform, or a simple weather app, you'll need to fetch data from servers.
Don't worry if networking sounds scary! ๐ฐ By the end of this article, you'll be handling REST APIs, managing errors gracefully, and implementing caching like a pro. Let's dive in! ๐
๐ What is Network Handling?
Think of network handling as your app's way of having conversations with the internet. Just like you text your friends, your app needs to "talk" to servers to:
- ๐ฅ Get data (like user posts, weather info, or product details)
- ๐ค Send data (like uploading photos or submitting forms)
- ๐ Update information (like editing your profile)
๐ ๏ธ Setting Up Your Flutter Project
Before we start coding, let's add the essential packages to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
http: ^1.1.0 # For making HTTP requests
dio: ^5.3.2 # Alternative HTTP client (more features)
connectivity_plus: ^5.0.1 # Check internet connection
shared_preferences: ^2.2.2 # For simple caching
Don't forget to run:
flutter pub get
๐ REST API Integration: Your First Network Call
What is REST? ๐ค
REST (Representational State Transfer) is like a universal language that apps and servers use to communicate. It uses simple HTTP methods:
- GET ๐: "Hey server, give me some data!"
- POST ๐: "Here's some new data for you!"
- PUT โ๏ธ: "Please update this data!"
- DELETE ๐๏ธ: "Remove this data!"
Making Your First HTTP Request ๐ฏ
Let's start with a simple example. We'll fetch some fake user data:
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
static const String baseUrl = 'https://jsonplaceholder.typicode.com';
// ๐ GET request - Fetch users
static Future<List<User>> getUsers() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/users'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
return jsonData.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users ๐');
}
} catch (e) {
throw Exception('Network error: $e');
}
}
// ๐ POST request - Create new user
static Future<User> createUser(User user) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/users'),
headers: {'Content-Type': 'application/json'},
body: json.encode(user.toJson()),
);
if (response.statusCode == 201) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to create user ๐');
}
} catch (e) {
throw Exception('Network error: $e');
}
}
}
Creating a User Model ๐ค
class User {
final int id;
final String name;
final String email;
final String phone;
User({
required this.id,
required this.name,
required this.email,
required this.phone,
});
// Convert JSON to User object
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] ?? 0,
name: json['name'] ?? '',
email: json['email'] ?? '',
phone: json['phone'] ?? '',
);
}
// Convert User object to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'phone': phone,
};
}
}
โ Error Handling: When Things Go Wrong
Network requests can fail for many reasons:
- ๐ต No internet connection
- ๐ Slow network
- ๐ซ Server is down
- ๐ Authentication issues
Let's create a robust error handling system:
Custom Exception Classes ๐ฏ
abstract class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}
class NoInternetException extends NetworkException {
NoInternetException() : super('No internet connection ๐ต');
}
class ServerException extends NetworkException {
ServerException(String message) : super('Server error: $message ๐จ');
}
class TimeoutException extends NetworkException {
TimeoutException() : super('Request timeout โฐ');
}
class UnauthorizedException extends NetworkException {
UnauthorizedException() : super('Unauthorized access ๐');
}
Enhanced API Service with Error Handling ๐ช
import 'package:connectivity_plus/connectivity_plus.dart';
class NetworkService {
static const Duration timeoutDuration = Duration(seconds: 30);
// Check internet connectivity
static Future<bool> hasInternetConnection() async {
final connectivityResult = await Connectivity().checkConnectivity();
return connectivityResult != ConnectivityResult.none;
}
// Enhanced GET request with error handling
static Future<http.Response> getRequest(String endpoint) async {
// Check internet connection first
if (!await hasInternetConnection()) {
throw NoInternetException();
}
try {
final response = await http
.get(
Uri.parse(endpoint),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeoutDuration);
return _handleResponse(response);
} on TimeoutException {
throw TimeoutException();
} catch (e) {
throw NetworkException('Unexpected error: $e ๐');
}
}
// Handle different HTTP status codes
static http.Response _handleResponse(http.Response response) {
switch (response.statusCode) {
case 200:
case 201:
return response;
case 401:
throw UnauthorizedException();
case 404:
throw NetworkException('Resource not found ๐');
case 500:
throw ServerException('Internal server error');
default:
throw ServerException('HTTP ${response.statusCode}');
}
}
}
User-Friendly Error Display ๐จ
class NetworkErrorWidget extends StatelessWidget {
final NetworkException exception;
final VoidCallback onRetry;
const NetworkErrorWidget({
Key? key,
required this.exception,
required this.onRetry,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Different icons for different errors
Icon(
_getErrorIcon(),
size: 80,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
exception.message,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Try Again'),
),
],
),
);
}
IconData _getErrorIcon() {
if (exception is NoInternetException) return Icons.wifi_off;
if (exception is TimeoutException) return Icons.access_time;
if (exception is UnauthorizedException) return Icons.lock;
return Icons.error;
}
}
๐พ Caching: Making Your App Lightning Fast
Caching is like having a smart memory for your app. Instead of asking the server for the same data repeatedly, you store it locally. This makes your app:
- โก Faster
- ๐ฑ Work offline
- ๐ฐ Use less mobile data
Simple Caching with SharedPreferences ๐๏ธ
class CacheService {
static const String _usersCacheKey = 'cached_users';
static const String _lastFetchKey = 'last_fetch_time';
static const Duration cacheExpiration = Duration(hours: 1);
// Save data to cache
static Future<void> cacheUsers(List<User> users) async {
final prefs = await SharedPreferences.getInstance();
final usersJson = users.map((user) => user.toJson()).toList();
await prefs.setString(_usersCacheKey, json.encode(usersJson));
await prefs.setInt(_lastFetchKey, DateTime.now().millisecondsSinceEpoch);
}
// Get data from cache
static Future<List<User>?> getCachedUsers() async {
final prefs = await SharedPreferences.getInstance();
// Check if cache exists and is not expired
if (!_isCacheValid(prefs)) {
return null;
}
final cachedData = prefs.getString(_usersCacheKey);
if (cachedData == null) return null;
final List<dynamic> jsonData = json.decode(cachedData);
return jsonData.map((json) => User.fromJson(json)).toList();
}
// Check if cache is still valid
static bool _isCacheValid(SharedPreferences prefs) {
final lastFetch = prefs.getInt(_lastFetchKey);
if (lastFetch == null) return false;
final lastFetchTime = DateTime.fromMillisecondsSinceEpoch(lastFetch);
final now = DateTime.now();
return now.difference(lastFetchTime) < cacheExpiration;
}
// Clear cache
static Future<void> clearCache() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_usersCacheKey);
await prefs.remove(_lastFetchKey);
}
}
Smart Data Service with Caching ๐ง
class UserDataService {
// Get users with cache-first strategy
static Future<List<User>> getUsers({bool forceRefresh = false}) async {
try {
// Try cache first (unless force refresh)
if (!forceRefresh) {
final cachedUsers = await CacheService.getCachedUsers();
if (cachedUsers != null) {
print('๐ฆ Loading from cache!');
return cachedUsers;
}
}
// Fetch from network
print('๐ Fetching from network...');
final users = await ApiService.getUsers();
// Save to cache
await CacheService.cacheUsers(users);
return users;
} catch (e) {
// If network fails, try cache as fallback
final cachedUsers = await CacheService.getCachedUsers();
if (cachedUsers != null) {
print('๐ฆ Using cached data as fallback');
return cachedUsers;
}
rethrow; // No cache available, rethrow the error
}
}
}
๐ฎ Putting It All Together: A Complete Example
Let's create a user list screen that demonstrates everything we've learned:
class UserListScreen extends StatefulWidget {
@override
_UserListScreenState createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
List<User> users = [];
bool isLoading = false;
NetworkException? error;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers({bool forceRefresh = false}) async {
setState(() {
isLoading = true;
error = null;
});
try {
final fetchedUsers = await UserDataService.getUsers(
forceRefresh: forceRefresh,
);
setState(() {
users = fetchedUsers;
isLoading = false;
});
} on NetworkException catch (e) {
setState(() {
error = e;
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Users ๐ฅ'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _loadUsers(forceRefresh: true),
),
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (isLoading && users.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading users... โณ'),
],
),
);
}
if (error != null && users.isEmpty) {
return NetworkErrorWidget(
exception: error!,
onRetry: () => _loadUsers(),
);
}
return RefreshIndicator(
onRefresh: () => _loadUsers(forceRefresh: true),
child: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(
child: Text(user.name[0].toUpperCase()),
),
title: Text(user.name),
subtitle: Text(user.email),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Navigate to user details
_showUserDialog(user);
},
);
},
),
);
}
void _showUserDialog(User user) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('๐ค ${user.name}'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('๐ง Email: ${user.email}'),
const SizedBox(height: 8),
Text('๐ Phone: ${user.phone}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
}
๐ง Advanced Tips and Best Practices
1. Use Dio for Advanced Features ๐
While the http
package is great for simple requests, Dio
offers more features:
import 'package:dio/dio.dart';
class DioService {
static final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
},
));
// Add interceptors for logging and error handling
static void setupInterceptors() {
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
// Global error handling
print('โ Global error: ${error.message}');
handler.next(error);
},
));
}
static Future<Response> get(String path) async {
return await _dio.get(path);
}
}
2. Implement Retry Logic ๐
class RetryableNetworkService {
static Future<T> withRetry<T>(
Future<T> Function() operation, {
int maxRetries = 3,
Duration delay = const Duration(seconds: 1),
}) async {
int attempts = 0;
while (attempts < maxRetries) {
try {
return await operation();
} catch (e) {
attempts++;
if (attempts >= maxRetries) {
rethrow; // Max attempts reached
}
print('๐ Retry attempt $attempts/$maxRetries');
await Future.delayed(delay * attempts); // Exponential backoff
}
}
throw Exception('This should never be reached');
}
}
// Usage
final users = await RetryableNetworkService.withRetry(
() => ApiService.getUsers(),
maxRetries: 3,
);
3. Background Sync ๐
class BackgroundSyncService {
static Future<void> syncWhenOnline() async {
final connectivity = Connectivity();
connectivity.onConnectivityChanged.listen((result) async {
if (result != ConnectivityResult.none) {
print('๐ Back online! Syncing data...');
await _syncPendingData();
}
});
}
static Future<void> _syncPendingData() async {
// Sync any pending offline data
// Upload queued requests
// Refresh cached data
}
}
๐ฏ Key Takeaways
Congratulations! ๐ You've learned the essentials of network handling in Flutter. Here's what you now know:
- ๐ REST API Integration: How to make GET, POST, PUT, and DELETE requests
- โ Error Handling: Creating robust error handling with custom exceptions
- ๐พ Caching: Implementing smart caching strategies for better performance
- ๐ Retry Logic: Adding resilience to your network requests
- ๐ฑ User Experience: Creating smooth, user-friendly interfaces
๐ Next Steps
Ready to level up even more? Consider exploring:
- ๐ State Management: Use Provider, Riverpod, or BLoC for better state handling
- ๐ Authentication: Implement JWT tokens and OAuth
- ๐ก WebSockets: For real-time communication
- ๐๏ธ Local Databases: SQLite or Hive for complex offline storage
- ๐งช Testing: Unit and integration tests for your network layer
๐ก Final Thoughts
Network handling might seem complex at first, but with practice, it becomes second nature. Remember:
- ๐ก๏ธ Always handle errors gracefully
- ๐พ Cache strategically for better performance
- ๐ฅ Think about the user experience
- ๐งช Test your network layer thoroughly
Top comments (0)