Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15 — Flutter Implementation Guide (Files Module)

How to build the Files module on the existing app architecture (00-shared/11_Flutter_App_Architecture, 00-shared/06_State_Management). Backend contract is fixed by 12_API_Mapping.md; state model by 13_State_Management.md. Deps: dio (transport, already core), path_provider, file_picker (or platform pickers), open_filex/share (forward-looking).


1. Upload with progress (dio)

Future<FileRecord> upload(PickedFile f, {void Function(int, int)? onProgress}) async {
  final form = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      f.path,
      filename: f.name,
      contentType: DioMediaType.parse(f.mimeType), // echo client type — server trusts it
    ),
  });
  final res = await api.post('/files/upload',
    data: form,
    onSendProgress: onProgress,
    options: Options(contentType: 'multipart/form-data'),
  );
  return FileRecord.fromJson(res.data['data']); // envelope per 00-shared/07
}
  • Server contract: multipart part named file (files.controller.ts:31), one file per request; field name is fixed — MultipartFile.fromFile must be wrapped in a map with key file.
  • Progress: dio onSendProgress gives (sent, total). Until the first event (server buffering), show indeterminate AppProgress (10_Interaction_Specification.md §3).
  • Cancel: CancelToken per upload; abort discards the body client-side (no server partial state).
  • Retry: re-POST the whole body; expect a new FileRecord (no idempotency).
  • Auth: attach Bearer token via the shared dio interceptor; guards are (planned), so test with a role that holds file.upload (permissions.constants.ts:87).

UploadCubit wiring

UploadIdle → UploadPicking → UploadSelected → UploadUploading(progress) → UploadSuccess | UploadFailed | cancelled (mermaid in 13_State_Management.md). On success call FilesCubit.upsertLocal(record); on failure keep the picked file for Retry.

2. List

final res = await api.get('/files');            // full tenant list, createdAt desc
final files = (res.data['data'] as List).map(FileRecord.fromJson).toList();
  • No pagination/filter params (files.service.ts:48-50) — load once, group client-side by context key in byContext (see 13_State_Management.md).

3. Download with progress

Future<String> download(FileRecord f, {void Function(int, int)? onProgress, CancelToken? cancel}) async {
  final dir = await getTemporaryDirectory();
  final path = '${dir.path}/${sanitizeForFs(f.originalName)}';
  await api.download('/files/${f.id}/download', path,
    onReceiveProgress: onProgress, cancelToken: cancel);
  return path;
}
  • Server: buffered proxied bytes with Content-Disposition: attachment (files.controller.ts:55-64) — dio downloads the body to disk; no redirect handling needed.
  • Cancel: CancelToken; no resume (no Range) — retry restarts from 0.
  • Open: open_filex / url_launcher for the temp path; Share via share_plus (forward-looking).
  • Filename: the server returns the original name only in the record + header; sanitize locally for filesystem safety (server does not sanitize, files.service.ts:31).

DownloadManagerSheet

Single DownloadCubit owning a task list (13_State_Management.md §1.3); sheet lists queued/active/completed tasks with per-task AppProgress and cancel.

4. Delete

await api.delete('/files/${f.id}');             // { message: 'File deleted' }
filesCubit.removeLocal(f.id);                    // optimistic, rollback on error
  • 404 → treat as already gone, remove row (files.service.ts:54).
  • Only render delete when the role holds file.delete (permissions.constants.ts:88).

5. Permissions & guards (planned)

  • Route decorators exist (files.controller.ts:30,44,50,56,67); RBAC guards not yet implemented. Client: read permission flags from the roles endpoint and hide/show Attach/Delete/Download accordingly; handle 403 uniformly once guards land.

6. Offline & outbox (forward-looking)

  • Upload/download actions disabled while offline (AppOfflineBanner); queue uploads in a local outbox and drain on reconnect.

7. Tests (per 00-shared/10_QA_Baseline + module QA 14_QA_Checklist.md)

  • Unit: UploadCubit state machine (cancel, retry, success path); byContext grouping.
  • Widget: FileTile labels; progress card percent rendering; delete dialog copy.
  • Integration (mock dio): multipart body contains part file; progress callback invoked; envelope unwrap; 404 handling.
  • E2E: upload → appears in list → download → file matches bytes → delete → gone.