Implementing Retry Logic in Flutter with HTTP Requests

Created At: 2025-02-27 06:57:31 Updated At: 2025-02-27 07:08:03

Introduction:

In this tutorial, we will learn how to implement retry logic for HTTP requests in a Flutter application. Retry logic ensures that if a request fails due to temporary issues (such as network unavailability or server timeout), it will automatically retry for a specified number of attempts.

This can significantly improve user experience by reducing failures and ensuring the app continues to function smoothly even under intermittent network conditions.

We will use the http package for making HTTP requests, and the retry package to handle automatic retries when the request fails. Additionally, we'll incorporate basic logging using print statements for debugging and monitoring the retry process.

Step 1: Set up your Flutter project

First, ensure that your Flutter project is set up and dependencies are installed. In this example, we'll be using the http and retry packages.

  1. Add dependencies to your pubspec.yaml:

Step 2: Define the retry logic

In this step, we will define a method fetchAppData that will make an HTTP GET request to fetch app data from a remote server. If the request fails, it will retry a specified number of times with a delay between each attempt.

import 'dart:async';

import 'dart:convert';

import 'package:package_info_plus/package_info_plus.dart';

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

import 'package:retry/retry.dart';

class AppSetupController extends GetxController {

 

  final RxString _appVersion = "0.0".obs;

  

  final Rx<AppSetupModel?> appSetup = Rx<AppSetupModel?>(null);

  final RxBool isLoading = false.obs;

  final RxInt _shopType = 2.obs;

  int get shopType => _shopType.value;

  RxBool showRetryButton = false.obs;

  Future fetchAppData() async {

    try {

      isLoading.value = true;

      // Retry up to 3 times with a 2-second delay between retries

      final response = await retry(

            () => http.get(Uri.parse('${Environment.appBaseUrl}/api/appSetup'))

            .timeout(Duration(seconds: 10)), // Timeout after 10 seconds

        retryIf: (e) => e is http.ClientException || e is TimeoutException, // Retry on specific exceptions

        maxAttempts: 3,

      );

      if (response.statusCode == 200) {

        final data = jsonDecode(response.body);

        if (data['status'] == true) {

          appSetup.value = AppSetupModel.fromJson(data['data']);

          // Set up values

         // set up things based on this call

          isLoading.value = false;

          return; // Data fetched successfully, exit early

        } else {

          print("Set up error: ${response.statusCode}");

          Get.snackbar("Error", "Failed to load app data.");

        }

      } else {

        Get.snackbar("Error", "Server Error: ${response.body}");

      }

    } catch (e, trace) {

      print("Error fetching app data: $trace");

      // After retry attempts, show retry button or error message

      Get.snackbar("Error", "An error occurred while fetching app data. Please   try again.");

    }

    isLoading.value = false;

    // Optionally, show the retry button if all attempts failed

    showRetryButton.value = true;

  }

}

class AppSetupModel {

  final String title;

  final double deliveryDistance;

  final String description;

  final String googleMapKey;

  final double deliveryFee;

  final String appCurrency;

  final double couponRate;

  final int shopType;

  AppSetupModel({

    required this.title,

    required this.deliveryDistance,

    required this.description,

    required this.googleMapKey,

    required this.deliveryFee,

    required this.appCurrency,

    required this.couponRate,

    required this.shopType,

  });

  factory AppSetupModel.fromJson(Map<String, dynamic> json) {

    return AppSetupModel(

      title: json['title'] ?? '',

      deliveryDistance: _toDouble(json['deliveryDistance'], defaultValue: 0.0),

      description: json['description'] ?? '',

      googleMapKey: json['googleMapKey'] ?? '',

      deliveryFee: _toDouble(json['deliveryFee'], defaultValue: 0.0),

      appCurrency: json['appCurrency'] ?? '',

      couponRate: _toDouble(json['couponRate'], defaultValue: 0.0),

      shopType: _toInt(json['shopType'], defaultValue: 2),

    );

  }

  static double _toDouble(dynamic value, {double defaultValue = 0.0}) {

    if (value == null) return defaultValue;

    if (value is num) return value.toDouble(); // Handles int and double values

    if (value is String) return double.tryParse(value) ?? defaultValue;

    return defaultValue;

  }

  static int _toInt(dynamic value, {int defaultValue = 2}) {

    if (value == null) return defaultValue;

    if (value is int) return value;

    if (value is String) return int.tryParse(value) ?? defaultValue;

    return defaultValue;

  }

}

Step 3: Explanation of the code

  1. Retry Logic:

    • We use the retry function from the retry package to handle retries automatically.
    • The retryIf condition checks for specific exceptions (ClientException and TimeoutException) and triggers a retry when these exceptions occur.
    • The retry will occur for a maximum of 3 attempts, with a 2-second delay between retries.
  2. Making the HTTP Request:

    • We use the http.get() method to send a GET request to the server.
    • The timeout() method ensures that if the server doesn't respond within 10 seconds, the request is canceled.
  3. Response Handling:

    • After receiving the response, we check the HTTP status code to ensure it is 200 OK.
    • If the response is successful, we parse the JSON data and perform any necessary actions (e.g., storing data or updating the UI).
  4. Error Handling:

    • If the request fails after the maximum number of retries, we print an error message and stop retrying.
    • If the request fails due to other reasons (e.g., server error), it will print the error and proceed.

Step 4: Displaying the Retry Button on Failure

After retrying a specified number of times, we may want to show a retry button to the user. This button allows the user to manually retry fetching the data if the automatic retries fail. Below is how you can integrate a retry button inside a Flutter UI:

import 'dart:async';

import 'package:flutter/animation.dart';

import 'package:flutter/cupertino.dart';

import 'package:flutter/material.dart';

import 'package:foodly_user/constants/routes_names.dart';

import 'package:foodly_user/controllers/app_setup_controller.dart';

import 'package:foodly_user/views/home/widgets/custom_btn.dart';

import 'package:get/get.dart';

class SplashScreen extends StatefulWidget {

  const SplashScreen({Key? key}) : super(key: key);

  @override

  _SplashScreenState createState() => _SplashScreenState();

}

class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMixin {

  late Animation<double> animation;

  late AnimationController _controller;

  final GlobalKey<ScaffoldState> _globalKey = GlobalKey();

  final AppSetupController _appSetupController = Get.put(AppSetupController());

  @override

  void initState() {

    super.initState();

    // Initialize animation controller

    _controller = AnimationController(

      vsync: this,

      duration: const Duration(seconds: 2),

    )..forward(); // Trigger animation forward immediately

    // Create an animation with linear curve

    animation = CurvedAnimation(parent: _controller, curve: Curves.linear);

    // Fetch data and then navigate

    fetchDataAndNavigate();

  }

  Future<void> fetchDataAndNavigate({int retries = 3, int delay = 2000}) async {

    int attempt = 0;

    while (attempt < retries) {

      try {

        print("Fetching app data... (Attempt ${attempt + 1})");

        await _appSetupController.fetchAppData();

        print("Fetching completed...");

        // After fetch, ensure data is loaded

        if (_appSetupController.appSetup.value != null) {

          print("App setup data loaded successfully");

          // Start animation listener and navigation logic once data is loaded

          _controller.addStatusListener((status) {

            print("Animation status: $status");

            if (status == AnimationStatus.completed) {

              print("Animation completed, navigating...");

              _navigateToNextScreen();

            }

          });

          Timer(Duration(milliseconds: 500), () {

            print("Checking animation status manually: ${_controller.status}");

            if (_controller.status == AnimationStatus.completed) {

              print("Manually detected animation completion, navigating...");

              _navigateToNextScreen();

            }

          });

          return; // Exit the function since data is successfully loaded

        }

        print("App setup data is null, retrying in ${delay / 1000} seconds...");

        attempt++;

        await Future.delayed(Duration(milliseconds: delay));

      } catch (e) {

        print("Error fetching app data (Attempt ${attempt + 1}): $e");

        if (attempt == retries - 1) {

          Get.snackbar("Error", "Failed to load app data after multiple attempts.");

        }

        attempt++;

        await Future.delayed(Duration(milliseconds: delay));

      }

    }

  }

// Ensures navigation happens after frame rendering completes

  void _navigateToNextScreen() {

      print("Navigating after animation completion...");

      Get.offNamed(RouteNames.getMainScreenRoute());

  }

  @override

  void dispose() {

    _controller.dispose();

    super.dispose();

  }

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      key: _globalKey,

      backgroundColor: Colors.white,

      body: Column(

        mainAxisAlignment: MainAxisAlignment.center,

        children: [

          ScaleTransition(

            scale: animation,

            child: Center(child: Image.asset("assets/images/logo.png", width: 200)),

          ),

          Obx(() {

            if (_appSetupController.isLoading.value) {

              return CircularProgressIndicator();

            } else if (_appSetupController.showRetryButton.value) {

              return Column(

                mainAxisAlignment: MainAxisAlignment.center,

                children: [

                  Text("Failed to load app data. Please try again."),

                  SizedBox(height: 20),

                  GestureDetector(

                    onTap:(){

                      fetchDataAndNavigate();

                   },

                    child: CustomButton(

                        btnHieght: 45,

                        btnWidth: 140,

                        text: "Retry"

                    ),

                  ),

                ],

              );

            } else {

              return Text('App setup data loaded successfully!');

            }

          }),

        ],

      ),

    );

  }

}

Step 5: Final Thoughts

By combining the retry logic with a retry button, you can give users an interactive way to deal with issues related to network failures or server downtime. The automatic retries will reduce the need for manual intervention, and the retry button offers users an alternative if automatic retries are exhausted.

This implementation allows the app to be more resilient to intermittent network issues and ensures that users have a smooth experience, even when the backend server is temporarily unavailable.

👆👆Build high performance app

👆👆Build scalable app

Comment

Add Reviews