DEV Community

Cover image for Effective Network Handling in Flutter: REST API Integration, Error Handling, and Caching.
Nkusi kevin
Nkusi kevin

Posted on

Effective Network Handling in Flutter: REST API Integration, Error Handling, and Caching.

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
Enter fullscreen mode Exit fullscreen mode

Don't forget to run:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

๐ŸŒ 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');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

โŒ 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 ๐Ÿ”');
}
Enter fullscreen mode Exit fullscreen mode

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}');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’พ 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฎ 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'),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ง 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
);
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ Key Takeaways

Congratulations! ๐ŸŽ‰ You've learned the essentials of network handling in Flutter. Here's what you now know:

  1. ๐Ÿ”Œ REST API Integration: How to make GET, POST, PUT, and DELETE requests
  2. โŒ Error Handling: Creating robust error handling with custom exceptions
  3. ๐Ÿ’พ Caching: Implementing smart caching strategies for better performance
  4. ๐Ÿ”„ Retry Logic: Adding resilience to your network requests
  5. ๐Ÿ“ฑ 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)