Search code examples
flutterrestdartmultipartform-datadio

Flutter Dio: How To Upload FormData/BulkImages Using A Structured Class/Modal Of REST APIs


My problem is different from a general concept. First checkout my current structure of Flutter App where I have to work without changing the structure.

api_service.dart

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:retrofit/http.dart';
import '/network/interceptor/logging_interceptor.dart';
import '/network/response/general_response.dart';
import 'app_url.dart';

part 'api_service.g.dart';

@RestApi(baseUrl: AppUrl.apiUrl)
abstract class ApiService {
  factory ApiService(Dio dio, baseUrl) {
    dio.options = BaseOptions(
        receiveTimeout: 50000,
        connectTimeout: 50000,
        followRedirects: false,
        validateStatus: (status) { return status! < 500; },
        headers: {
          'Authorization': 'Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
        });

    dio.interceptors.add(Logging(dio: dio));
    return _ApiService(dio, baseUrl: AppUrl.apiUrl);
  }

  // APIs EndPoints Request Bodies without Token In Header As It Is Added In LoggingInterceptor 

  @POST('/login')
  Future<GeneralResponse> login(@Body() Map<String, dynamic> body);

  @POST('/signup')
  Future<GeneralResponse> signup(@Body() Map<String, dynamic> body);

  @POST('/json_receive')
  Future<GeneralResponse> json_receive(@Body() Map<String, dynamic> body);

  @POST('/simple_multipart_receiving')
  @MultiPart()
  Future<GeneralResponse> simple_multipart_receiving(
      @Part(name: 'id') int id,
      @Part(name: 'data') String data,
      @Part(name: 'images') File image,
 );

  @POST('/formdata_receiving')
  @MultiPart()
  Future<GeneralResponse> formdata_receiving(formData);
}

api_url.dart

import '/network/api_service.dart';
import 'package:dio/dio.dart' as dio;

class AppUrl {
  static const String apiUrl = 'http://192.168.1.1/demo/api/';
  static ApiService apiService = ApiService(dio.Dio(),AppUrl.apiUrl);
}

logging_interceptor.dart

import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '/theme/color.dart';
import '/utility/shared_preference.dart';
import '/utility/top_level_variables.dart';
import '../../dialog/error_dialog.dart';

class Logging extends Interceptor {
  String endpoint = "";
  final Dio dio;

  Logging({
    required this.dio,
  });

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    endpoint = options.path;
    options.baseUrl = UserPreferences.baseUrl.isNotEmpty ? UserPreferences.baseUrl : "http://192.168.1.1/demo/api/" ;
    if (options.path != '/login' &&
        options.path != '/signup') {
      options.headers.addEntries([MapEntry("token", UserPreferences.AuthToken)]);
    }
    if (options.path == '/formdata_receiving' || options.path == '/simple_multipart_receiving') {
      options.contentType = 'multipart/form-data';
    }
    return super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    if (response.data['message'] == 'Invalid Token or Token Expired') {
      _tokenExpiredDialog(TopVariables.appNavigationKey.currentContext!);
    }
    else if (response.data['message'] == 'Invalid API Authorization') {
      showErrorDialog(response.data['message']);
    }
    else if (response.data['error'] == 1) {
      showErrorDialog(response.data['message'].toString());
    }
    return super.onResponse(response, handler);
  }

  @override
  Future<void> onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 200) {
      // Everything is Alright
    }
    else if (err.response?.statusCode == 101) {
      Navigator.pop(TopVariables.appNavigationKey.currentContext!);
    } else if (err.response?.statusCode == 404) {
      showErrorDialog(err.error);
    }
    else if (err.response?.statusCode == 500) {
      showErrorDialog(err.error);
    }
    else {
      showErrorDialog(err.toString());
    }
    //return super.onError(err, handler);

    if (_shouldRetryOnHttpException(err)) {
      try {
        handler.resolve(await DioHttpRequestRetrier(dio: dio).requestRetry(err.requestOptions).catchError((e) {
          handler.next(err);
        }));
      } catch (e) {
        handler.next(err);
      }
    } else {
      handler.next(err);
    }
  }

  bool _shouldRetryOnHttpException(DioError err) {
    return err.type == DioErrorType.other && ((err.error is HttpException && err.message.contains('Connection closed before full header was received')));
  }
}

_tokenExpiredDialog(BuildContext context) async {
  return showDialog<void>(
    context: context,
    barrierDismissible: false, // User Must Tap Button
    builder: (BuildContext context) {
      return WillPopScope(
          onWillPop: () async => false,
          child: AlertDialog(
              contentPadding: EdgeInsets.zero,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10.0),
              ),
              content: SingleChildScrollView(
                child: Column(
                  children: <Widget>[
                    Container(
                      padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
                      width: MediaQuery.of(context).size.width,
                      decoration: BoxDecoration(
                          borderRadius: BorderRadius.only(
                            topRight: Radius.circular(10.0),
                            topLeft: Radius.circular(10.0),
                          ),
                          color: CustomColor.primary),
                      child: Text(
                        'Authentication Expired',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          color: CustomColor.white,
                        ),
                      ),
                    ),
                    SizedBox(
                      height: 10,
                    ),
                    Container(
                      padding: const EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 0.0),
                      child: Text(
                        'Your Authentication Has Been Expired. Please Login Again.',
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ],
                ),
              ),
              actions: <Widget>[
                Container(
                  alignment: Alignment.center,
                  child: TextButton(
                    child: const Text(
                      'Ok',
                      style: TextStyle(color: CustomColor.white),
                    ),
                    style: ButtonStyle(
                        backgroundColor: MaterialStateProperty.all(CustomColor.primary),
                        shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                            RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)))),
                    onPressed: () async {
                      prefs.clear();
                      Navigator.of(context).pushNamedAndRemoveUntil('/SignIn', (Route<dynamic> route) => false);
                    },
                  ),
                )
              ]));
    },
  );
}

/// Dio Retrier
class DioHttpRequestRetrier {
  final Dio dio;

  DioHttpRequestRetrier({
    required this.dio,
  });

  Future<Response> requestRetry(RequestOptions requestOptions) async {
    return dio.request(
      requestOptions.path,
      cancelToken: requestOptions.cancelToken,
      data: requestOptions.data,
      onReceiveProgress: requestOptions.onReceiveProgress,
      onSendProgress: requestOptions.onSendProgress,
      queryParameters: requestOptions.queryParameters,
      options: Options(
        contentType: requestOptions.contentType,
        headers: requestOptions.headers,
        sendTimeout: requestOptions.sendTimeout,
        receiveTimeout: requestOptions.receiveTimeout,
        extra: requestOptions.extra,
        followRedirects: requestOptions.followRedirects,
        listFormat: requestOptions.listFormat,
        maxRedirects: requestOptions.maxRedirects,
        method: requestOptions.method,
        receiveDataWhenStatusError: requestOptions.receiveDataWhenStatusError,
        requestEncoder: requestOptions.requestEncoder,
        responseDecoder: requestOptions.responseDecoder,
        responseType: requestOptions.responseType,
        validateStatus: requestOptions.validateStatus,
      ),
    );
  }
}

Now I am using the below code to call it and submit to REST API.

This Is How I Call It And Working

Map<String,dynamic> reqBody = {
  'organisation_id': UserPreferences.OrganizationId,
  'id': incomingMap["id"],
  'data': incomingMap["data"],
  'status': 00,
};
ApiService apiService = ApiService(dio.Dio(), AppUrl.apiUrl);
GeneralResponse response = await apiService.json_receive(reqBody);

This Is How I Call FORMDATA But Not Working, Why

var formData = {
  'organisation_id': UserPreferences.OrganizationId,
  'id': incomingMap["id"],
  'data': incomingMap["data"],
  'status': 00,
  'files': [
    MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
    MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
  ]
};
ApiService apiService = ApiService(dio.Dio(), AppUrl.apiUrl);
GeneralResponse response = await apiService.formdata_receiving(formData);

Solution

  • Problem is solved with adding few lines of codes. Here are they. Use the rest of the code as shared above and replace the last 2 code set with the below one.

    api_service.dart

    @POST('/formdata_receiving')
      @MultiPart()
      Future<GeneralResponse> formdata_receiving(@Body() FormData formData);
    

    This Is How I Call FORMDATA

    var formData = FormData.fromMap({
      'organisation_id': UserPreferences.OrganizationId,
      'id': incomingMap["id"],
      'data': incomingMap["data"],
      'status': 00,
      'files': [
        MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
        MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
      ]
    });
    ApiService apiService = ApiService(dio.Dio(), AppUrl.apiUrl);
    GeneralResponse response = await apiService.formdata_receiving(formData);
    flutter
    

    All is working like a charm now.