Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions templates/flutter/base/lib/src/services/services.dart.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export 'auth_service.dart';
export 'internet_connection_service.dart';
{{#if flags.usesDio}}
export 'dio_service.dart';
export 'upload_file_payload.dart';
export 'upload_progress.dart';
{{/if}}
{{#if flags.usesHttp}}
export 'http_service.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import '../config/app_config.dart';
import '../utils/utils.dart';
import 'upload_file_payload.dart';
import 'upload_progress.dart';

/// A robust networking service powered by Dio.
class DioService {
Expand Down Expand Up @@ -28,4 +30,83 @@ class DioService {
FutureEither<Response> delete(String path, {dynamic data, Map<String, dynamic>? queryParameters}) {
return runTask(() => AppConfig.dio.delete(path, data: data, queryParameters: queryParameters), requiresNetwork: true);
}

// --- Upload Methods ---

FutureEither<Response> uploadFile(
String path,
UploadFilePayload payload, {
void Function(UploadProgress)? onProgress,
Map<String, dynamic>? fields,
Map<String, dynamic>? queryParameters,
}) {
return uploadFiles(
path,
[payload],
onProgress: onProgress,
fields: fields,
queryParameters: queryParameters,
);
}

FutureEither<Response> uploadFiles(
String path,
List<UploadFilePayload> payloads, {
void Function(UploadProgress)? onProgress,
Map<String, dynamic>? fields,
Map<String, dynamic>? queryParameters,
}) {
return runTask(
() async {
final formData = await _buildFormData(payloads, fields: fields);
return AppConfig.dio.post(
path,
data: formData,
queryParameters: queryParameters,
options: _multipartOptions(),
onSendProgress: (sent, total) {
onProgress?.call(UploadProgress(sent: sent, total: total));
},
);
},
requiresNetwork: true,
);
}

// --- Private Helpers ---

/// Clears the global `application/json` Content-Type header for this request.
/// Dio automatically writes `multipart/form-data; boundary=...` for FormData.
Options _multipartOptions({Duration? sendTimeout}) => Options(
headers: {Headers.contentTypeHeader: null},
sendTimeout: sendTimeout ?? const Duration(minutes: 5),
);

/// Builds a [FormData] from [payloads] and optional string [fields].
/// Uses [formData.files.add] to correctly handle duplicate field names.
Future<FormData> _buildFormData(
List<UploadFilePayload> payloads, {
Map<String, dynamic>? fields,
}) async {
if (payloads.isEmpty) {
throw ArgumentError.value(payloads, 'payloads', 'At least one payload is required.');
}
final formData = FormData();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (fields != null) {
for (final entry in fields.entries) {
formData.fields.add(MapEntry(entry.key, entry.value.toString()));
}
}

final files = await Future.wait(
payloads.map((p) async => MapEntry(p.fieldName, await p.toMultipartFile())),
);

for (final file in files) {
formData.files.add(file);
}

return formData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:http_parser/http_parser.dart';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that generated Flutter pubspec templates declare http_parser directly.

set -eu

printf 'Import sites:\n'
rg -n -C2 "package:http_parser/http_parser.dart" --iglob '*.hbs' --iglob '*.dart' || true

printf '\nDirect dependency declarations expected under dependencies:\n'
rg -n -C3 "^[[:space:]]*http_parser:" --iglob 'pubspec*.hbs' --iglob 'pubspec.yaml' || true

Repository: Arjun544/flutter_init

Length of output: 720


Add http_parser as a direct dependency in the pubspec template.

Line 3 imports package:http_parser/http_parser.dart, but http_parser is not declared as a direct dependency in the pubspec templates. Relying solely on Dio's transitive dependency violates the depend_on_referenced_packages lint rule and makes generated apps fragile if Dio changes its dependency graph. Add http_parser to the dependencies section of the pubspec template.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@templates/flutter/overlays/networking/dio/lib/src/services/upload_file_payload.dart.hbs`
at line 3, The template imports package:http_parser/http_parser.dart but
http_parser is not declared as a direct dependency in the Flutter pubspec
template; update the pubspec template's dependencies section to add the
http_parser package (e.g., add "http_parser: ^<latest-compatible-version>") so
generated apps don't rely on Dio's transitive dependency and satisfy the
depend_on_referenced_packages lint—make the change in the pubspec template used
by the project alongside the import in
templates/flutter/overlays/networking/dio/lib/src/services/upload_file_payload.dart.hbs.


class UploadFilePayload {
final String? filePath;
final Uint8List? bytes;
final String fieldName;
final String? fileName;
final String? mimeType;

const UploadFilePayload._({
this.filePath,
this.bytes,
required this.fieldName,
this.fileName,
this.mimeType,
});

factory UploadFilePayload({
String? filePath,
Uint8List? bytes,
String fieldName = 'file',
String? fileName,
String? mimeType,
}) {
if (filePath == null && bytes == null) {
throw ArgumentError('UploadFilePayload requires either filePath or bytes.');
}
return UploadFilePayload._(
filePath: filePath,
bytes: bytes,
fieldName: fieldName,
fileName: fileName,
mimeType: mimeType,
);
}

String get _resolvedFileName =>
fileName ??
(filePath != null
? filePath!.split(RegExp(r'[/\\]')).last
: 'upload');

MediaType? get _mediaType =>
mimeType != null ? MediaType.parse(mimeType!) : null;

Future<MultipartFile> toMultipartFile() async {
if (bytes != null) {
return MultipartFile.fromBytes(
bytes!,
filename: _resolvedFileName,
contentType: _mediaType,
);
}
return MultipartFile.fromFile(
filePath!,
filename: _resolvedFileName,
contentType: _mediaType,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class UploadProgress {
final int sent;
final int total;

const UploadProgress({required this.sent, required this.total});

/// 0.0–1.0. Returns 0.0 when [total] is unknown (server omits Content-Length).
double get percentage =>
total > 0 ? (sent / total).clamp(0.0, 1.0) : 0.0;

/// True when the server did not return Content-Length.
bool get isIndeterminate => total <= 0;

@override
String toString() =>
'UploadProgress(sent: $sent, total: $total, '
'${(percentage * 100).toStringAsFixed(1)}%)';
}