flutter
/

GetX Network Retry: Robust HTTP Request Handling

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Network Retry: Robust HTTP Request Handling

Introduction

Network requests can fail for many reasons: no internet, timeouts, server errors (5xx), or rate limiting. A robust app should be able to retry these requests automatically or give users the option to retry. GetX, combined with an HTTP client like Dio, provides powerful tools to implement retry logic with minimal boilerplate. This guide covers strategies for automatic retries with exponential backoff, user‑triggered retries, and integrating retry state into your GetX controllers.

  1. Simple Retry with Recursion

The simplest way to implement a retry is to call the same function recursively on failure. This gives you full control over the retry count and delay.

DARTRead-only
1
Future<T> retry<T>(Future<T> Function() action, {int retries = 3, Duration delay = const Duration(seconds: 1)}) async {
  try {
    return await action();
  } catch (e) {
    if (retries <= 0) rethrow;
    await Future.delayed(delay);
    return retry(action, retries: retries - 1, delay: delay * 2); // exponential backoff
  }
}

// Usage in a controller
Future<void> fetchData() async {
  try {
    final data = await retry(() => api.getData());
    // success
  } catch (e) {
    // all retries failed
  }
}

  1. Using Dio Interceptor for Retry

If you use Dio, the dio_retry package provides a ready‑made interceptor. You can also write your own interceptor for fine‑grained control.

YAMLRead-only
1
dependencies:
  dio: ^5.4.0
  dio_retry: ^5.0.0
DARTRead-only
1
import 'package:dio/dio.dart';
import 'package:dio_retry/dio_retry.dart';

class ApiService extends GetxService {
  late Dio _dio;

  @override
  void onInit() {
    super.onInit();
    _dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
    _dio.interceptors.add(
      RetryInterceptor(
        dio: _dio,
        retries: 3,
        retryDelays: const [
          Duration(seconds: 1),
          Duration(seconds: 2),
          Duration(seconds: 4),
        ],
        retryEvaluator: (DioException error, int attempt) {
          // Retry on network errors or 5xx server errors
          return error.type == DioExceptionType.connectionTimeout ||
              error.type == DioExceptionType.receiveTimeout ||
              (error.response?.statusCode ?? 0) >= 500;
        },
      ),
    );
  }
}

  1. Exponential Backoff with Jitter

Exponential backoff (increasing delay between retries) prevents overwhelming the server. Adding a small random jitter helps avoid thundering herd problems when many clients retry simultaneously.

DARTRead-only
1
Future<T> retryWithBackoff<T>(Future<T> Function() action, {int maxRetries = 3, Duration initialDelay = const Duration(seconds: 1)}) async {
  int attempts = 0;
  Duration delay = initialDelay;

  while (true) {
    try {
      return await action();
    } catch (e) {
      attempts++;
      if (attempts > maxRetries) rethrow;
      // Add jitter (±20%)
      final jitter = Duration(milliseconds: (delay.inMilliseconds * 0.2).toInt());
      final actualDelay = delay + Duration(milliseconds: Random().nextInt(jitter.inMilliseconds));
      await Future.delayed(actualDelay);
      delay = delay * 2; // exponential
    }
  }
}

  1. User‑Triggered Retry with GetX

Sometimes you want to let the user decide when to retry (e.g., after showing an error message). Use a reactive boolean to indicate a retry state and expose a method that the UI can call.

DARTRead-only
1
class DataController extends GetxController {
  var data = ''.obs;
  var isLoading = false.obs;
  var error = ''.obs;

  Future<void> fetchData() async {
    isLoading.value = true;
    error.value = '';
    try {
      final result = await api.getData();
      data.value = result;
    } catch (e) {
      error.value = e.toString();
      // No automatic retry; user must tap a retry button
    } finally {
      isLoading.value = false;
    }
  }

  void retry() => fetchData(); // called from UI
}

// In UI
Obx(() {
  if (controller.isLoading.value) return CircularProgressIndicator();
  if (controller.error.value.isNotEmpty) {
    return Column(
      children: [
        Text('Error: ${controller.error.value}'),
        ElevatedButton(
          onPressed: controller.retry,
          child: Text('Retry'),
        ),
      ],
    );
  }
  return Text(controller.data.value);
});

  1. Automatic Retry with GetX Workers

You can combine a retry mechanism with a worker that listens to an error state and automatically retries after a delay.

DARTRead-only
1
class DataController extends GetxController {
  var data = ''.obs;
  var error = ''.obs;
  var retryCount = 0.obs;
  var isRetrying = false.obs;

  @override
  void onInit() {
    super.onInit();
    // Listen to error and automatically retry (max 3 times)
    ever(error, (err) async {
      if (err.isNotEmpty && retryCount.value < 3 && !isRetrying.value) {
        isRetrying.value = true;
        await Future.delayed(Duration(seconds: 2));
        retryCount.value++;
        await fetchData();
        isRetrying.value = false;
      }
    });
  }

  Future<void> fetchData() async {
    error.value = '';
    try {
      final result = await api.getData();
      data.value = result;
      retryCount.value = 0; // reset on success
    } catch (e) {
      error.value = e.toString();
    }
  }
}

  1. Showing Retry Progress

When retrying automatically, you may want to show a countdown or a message like "Retrying in X seconds…" to inform the user.

DARTRead-only
1
class DataController extends GetxController {
  var data = ''.obs;
  var error = ''.obs;
  var retryDelay = 0.obs;
  Timer? _timer;

  Future<void> fetchData() async {
    error.value = '';
    try {
      final result = await api.getData();
      data.value = result;
    } catch (e) {
      error.value = e.toString();
      _startRetryCountdown();
    }
  }

  void _startRetryCountdown() {
    retryDelay.value = 3;
    _timer?.cancel();
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (retryDelay.value <= 1) {
        timer.cancel();
        fetchData();
      } else {
        retryDelay.value--;
      }
    });
  }

  @override
  void onClose() {
    _timer?.cancel();
    super.onClose();
  }
}

// UI
Obx(() {
  if (controller.error.value.isNotEmpty) {
    return Column(
      children: [
        Text('Error: ${controller.error.value}'),
        Text('Retrying in ${controller.retryDelay.value}s...'),
        ElevatedButton(
          onPressed: controller.fetchData,
          child: Text('Retry now'),
        ),
      ],
    );
  }
  // ...
});

  1. Handling Idempotent Requests

Not all requests are safe to retry automatically (e.g., POST requests that create resources). Always check if the request is idempotent before retrying. For Dio, you can check the HTTP method.

DARTRead-only
1
bool shouldRetry(DioException error, int attempt) {
  // Only retry GET, HEAD, PUT, DELETE (idempotent methods)
  final isIdempotent = error.requestOptions.method == 'GET' ||
      error.requestOptions.method == 'HEAD' ||
      error.requestOptions.method == 'PUT' ||
      error.requestOptions.method == 'DELETE';
  if (!isIdempotent) return false;

  // Retry on network errors or 5xx
  return error.type == DioExceptionType.connectionTimeout ||
      error.type == DioExceptionType.receiveTimeout ||
      (error.response?.statusCode ?? 0) >= 500;
}

Best Practices

  • Limit retries – Usually 3‑5 attempts maximum.
  • Use exponential backoff – Prevents overwhelming the server.
  • Add jitter – Avoids thundering herd problems.
  • Only retry idempotent requests – Avoid duplicating non‑idempotent operations (e.g., POST).
  • Notify the user – Show a message when retrying automatically, or provide a manual retry button.
  • Cancel retries on navigation – If the user leaves the screen, cancel ongoing retry timers.
  • Test retry logic – Simulate network failures to ensure retries work correctly.

Common Mistakes

  • ❌ Retrying POST/PATCH requests – May create duplicate records. ✅ Only retry idempotent requests.
  • ❌ Infinite retries – Without a max limit, may loop forever. ✅ Always set a maximum retry count.
  • ❌ Immediate retries – Causes cascading failures; always add a delay. ✅ Use backoff strategies.
  • ❌ Not handling retry cancellation – Timers may fire after the controller is disposed. ✅ Cancel timers in onClose.

FAQ

  • Q: Should I retry on 401 (Unauthorized)?
    A: Usually no. 401 means the token is invalid; you should refresh the token or redirect to login.
  • Q: How do I retry only on certain error types?
    A: In your retry logic, check the exception type or status code and return true only for those you want to retry.
  • Q: Can I combine automatic and manual retry?
    A: Yes, provide a manual retry button that resets the retry counter and calls the same fetch method.
  • Q: Does dio_retry work with GetX?
    A: Yes, just add it as an interceptor in your ApiService. It will automatically retry according to your configuration.
  • Q: How to test retry logic?
    A: Use mockito to throw exceptions on the first few calls, then succeed on the third. Verify the number of attempts.

Conclusion

Implementing network retry is crucial for building resilient Flutter apps. GetX provides the flexibility to implement both automatic and manual retries, integrate with Dio interceptors, and manage retry state reactively. By following the patterns in this guide, you can ensure your app gracefully recovers from transient network issues.

Test Your Knowledge

Q1
of 3

Which HTTP methods are generally considered safe to retry automatically?

A
POST, PATCH
B
GET, HEAD, PUT, DELETE
C
All methods
D
Only GET
Q2
of 3

What is the purpose of exponential backoff?

A
To increase retry speed
B
To prevent overwhelming the server
C
To reduce code complexity
D
To handle user input
Q3
of 3

How do you ensure retry timers don't fire after a controller is disposed?

A
Cancel timers in onClose
B
Use a global variable
C
Do nothing, it's automatic
D
Use async/await

Previous

getx global error handler

Next

getx dynamic ui rendering

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.