uploadFile method
Uploads bytes for a file field and applies the outcome.
Success goes through change — a single-file field's value becomes the stored path; a multiple field's value gains it, one append per upload call, because the endpoint serves one file per request and the client loops. Either way any stale error on the field clears the same way any other edit clears one.
Failure routes on UploadFailed.statusCode, the same way submit
routes a write's 422 by field name: a 422 is this field's own
refusal (too large, wrong type) and lands as its error, value
untouched — a failed upload must never clear a file the record already
has. Anything else — a bare 403/500, an offline transport, a host that
never implemented FilamentUploadTransport — is not a fact about what
the user picked, so it reaches formError instead, same as an
unmappable write failure.
Implementation
Future<void> uploadFile(
String name, {
required List<int> bytes,
required String filename,
}) async {
final result = await source.uploadFile(
resource.key,
name,
bytes: bytes,
filename: filename,
);
// Disposal can land during the await — the user backed out of the form
// mid-upload. Guarded here, where the asymmetry is: both failure
// branches already notify through _notify(), but success routes through
// [change], whose bare notifyListeners() asserts on a disposed
// ChangeNotifier. change() itself stays unguarded on purpose — its
// other callers are synchronous taps from a live widget, and a
// silently-no-op change() would hide a real lifecycle bug there.
if (_disposed) return;
switch (result) {
case UploadSuccess(:final path):
change(name, _uploadedValue(name, path));
case UploadFailed(:final message, :final statusCode)
when statusCode == 422:
_fieldErrors = Map.unmodifiable({
..._fieldErrors,
name: message ?? strings.uploadFailed,
});
_notify();
case UploadFailed(:final message):
_formError = (message == null || message.isEmpty)
? strings.uploadFailed
: message;
_notify();
}
}