Repository: cfug/dio
Pull request #2015 (2023-10-30 19:10:02)

feat(dio): Allow ResponseDecoder and RequestEncoder to be async
<!-- Write down your pull request descriptions. -->

### New Pull Request Checklist

- [x] I have read the [Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the [project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid conflicts (via merge from master or rebase)
- [x] I have added the required tests to prove the fix/feature I'm adding
- [ ] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [x] I have updated the `CHANGELOG.md` in the corresponding package

### Additional context and info (if any)

Gives more flexibility to the developer if they want to do some async work in the response decoder. I don't think tests are needed for this. I don't _think_ this is a breaking change as the return type of `ResponseDecoder` is now `FutureOr<String?>`.


AlexV525: Could we address `ResponseEncoder` too? If so, https://github.com/cfug/dio/issues/1709 should be resolved.
AlexV525: Sorry, I mean the `RequestEncoder`. :)
AlexV525: > EDIT 2: I can allow RequestEncoder to return a Future in this PR, or I can open another one. Up to you guys.

@Reprevise Please continue with this PR if possible, thanks.
Reprevise: @AlexV525 `RequestEncoder` has been updated to return a FutureOr<List<int>>
AlexV525: You seem to be using a conflicted main branch, which merges your changes unexpectedly.
Reprevise: Should be fixed now.
Reprevise: Changelog has been updated

Patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index 0e30b88..b361c1b 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -7,6 +7,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
 
 - Raise warning for `Map`s other than `Map<String, dynamic>` when encoding request data.
 - Improve exception messages
+- Allow `ResponseDecoder` and `RequestEncoder` to be async
 
 ## 5.3.3
 
diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart
index 89f20fd..0387540 100644
--- a/dio/lib/src/dio_mixin.dart
+++ b/dio/lib/src/dio_mixin.dart
@@ -627,7 +627,13 @@ abstract class DioMixin implements Dio {
           // Call the request transformer.
           final transformed = await transformer.transformRequest(options);
           if (options.requestEncoder != null) {
-            bytes = options.requestEncoder!(transformed, options);
+            final encoded = options.requestEncoder!(transformed, options);
+
+            if (encoded is Future) {
+              bytes = await encoded;
+            } else {
+              bytes = encoded;
+            }
           } else {
             // Converts the data to UTF-8 by default.
             bytes = utf8.encode(transformed);
diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart
index c4aa0c2..d455afc 100644
--- a/dio/lib/src/options.dart
+++ b/dio/lib/src/options.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
 import 'package:meta/meta.dart';
 
 import 'adapter.dart';
@@ -76,14 +78,14 @@ enum ListFormat {
 typedef ValidateStatus = bool Function(int? status);
 
 /// The type of a response decoding callback.
-typedef ResponseDecoder = String? Function(
+typedef ResponseDecoder = FutureOr<String?> Function(
   List<int> responseBytes,
   RequestOptions options,
   ResponseBody responseBody,
 );
 
 /// The type of a response encoding callback.
-typedef RequestEncoder = List<int> Function(
+typedef RequestEncoder = FutureOr<List<int>> Function(
   String request,
   RequestOptions options,
 );
diff --git a/dio/lib/src/transformers/sync_transformer.dart b/dio/lib/src/transformers/sync_transformer.dart
index 54da8cc..e2c6f33 100644
--- a/dio/lib/src/transformers/sync_transformer.dart
+++ b/dio/lib/src/transformers/sync_transformer.dart
@@ -125,11 +125,17 @@ class SyncTransformer extends Transformer {
     );
     final String? response;
     if (options.responseDecoder != null) {
-      response = options.responseDecoder!(
+      final decodeResponse = options.responseDecoder!(
         responseBytes,
         options,
         responseBody..stream = Stream.empty(),
       );
+
+      if (decodeResponse is Future) {
+        response = await decodeResponse;
+      } else {
+        response = decodeResponse;
+      }
     } else if (!isJsonContent || responseBytes.isNotEmpty) {
       response = utf8.decode(responseBytes, allowMalformed: true);
     } else {


Test patch:
diff --git a/dio/test/options_test.dart b/dio/test/options_test.dart
index 0a30464..c31229b 100644
--- a/dio/test/options_test.dart
+++ b/dio/test/options_test.dart
@@ -1,4 +1,6 @@
 @TestOn('vm')
+import 'dart:convert';
+
 import 'package:dio/dio.dart';
 import 'package:test/test.dart';
 
@@ -399,6 +401,50 @@ void main() {
     expect(response.data, null);
   });
 
+  test('responseDecoder can return Future<String?>', () async {
+    final dio = Dio();
+    dio.options.responseDecoder = (_, __, ___) => Future.value('example');
+    dio.options.baseUrl = EchoAdapter.mockBase;
+    dio.httpClientAdapter = EchoAdapter();
+
+    final Response response = await dio.get('');
+
+    expect(response.data, 'example');
+  });
+
+  test('responseDecoder can return String?', () async {
+    final dio = Dio();
+    dio.options.responseDecoder = (_, __, ___) => 'example';
+    dio.options.baseUrl = EchoAdapter.mockBase;
+    dio.httpClientAdapter = EchoAdapter();
+
+    final Response response = await dio.get('');
+
+    expect(response.data, 'example');
+  });
+
+  test('requestEncoder can return Future<List<int>>', () async {
+    final dio = Dio();
+    dio.options.requestEncoder = (data, _) => Future.value(utf8.encode(data));
+    dio.options.baseUrl = EchoAdapter.mockBase;
+    dio.httpClientAdapter = EchoAdapter();
+
+    final Response response = await dio.get('');
+
+    expect(response.statusCode, 200);
+  });
+
+  test('requestEncoder can return List<int>', () async {
+    final dio = Dio();
+    dio.options.requestEncoder = (data, _) => utf8.encode(data);
+    dio.options.baseUrl = EchoAdapter.mockBase;
+    dio.httpClientAdapter = EchoAdapter();
+
+    final Response response = await dio.get('');
+
+    expect(response.statusCode, 200);
+  });
+
   test('invalid response type throws exceptions', () async {
     final dio = Dio(
       BaseOptions(