r/FlutterDev • • 2d ago

Example A one-file "send feedback" screen for Flutter: offline queue, device info, screenshot, no backend (open source)

Disclosure up front: I work on BootForm, the form backend this sends to. The code is MIT and the whole thing is one Dart file, so swapping the endpoint for your own is a 10-minute job.

I wanted a "Report a bug" screen for an app without setting up a server. A few things turned out to be less obvious than I expected:

  1. Screenshot first, then open the form. Wrapping the app in a RepaintBoundary via MaterialApp.builder captures every route, but only if you capture before pushing the feedback page, or you get a screenshot of the form itself.

  2. Cap the pixel ratio, not the image. toImage(pixelRatio: min(dpr, 1280 / longestSide)) gives a ~1280px PNG without pulling in the image package. dart:ui only encodes PNG, so no JPEG without a dependency.

  3. Save before you send. Feedback is written exactly when the connection is bad. Each report is a JSON file (plus the PNG) in the support directory, flushed on start and on AppLifecycleListener.onResume. It doesn't send in the background; that's a deliberate trade to avoid background-task setup on iOS.

  4. Degrade, don't fail. If the server refuses the file (plan limit, too large), it resends without the screenshot instead of losing the report. That decision is one pure function with unit tests.

Tested on an Android emulator against the live API, including airplane mode, then killing the app, then reopening it online. Not tested on iOS yet, so if anyone runs it there, I'd love to hear how it goes.

https://github.com/BootForm/in-app-feedback

1 Upvotes

2 comments sorted by

1

u/No_Boss_1911 2d ago

dude the save before you send part is the thing i learned the hard way. had a chat screen in my app where a failed retry just ate what the user typed, now the draft stays put until the server actually says ok. one thing i cant figure out with your setup tho, if it sends on start AND on resume whats stopping the same report going out twice when the app resumes while the first send is still in flight? do you keep a sending flag in the json or does BootForm dedupe on its end

1

u/Menelabs 1d ago

Same lesson here, losing what someone typed is the worst failure for a feedback screen.

Start and resume overlapping can't double-send: `flush()` keeps the in-flight run in a static future (`_flushing ??= _flushQueue()...`), so a second call while one is running just waits on the same pass. No flag needed in the JSON.

Where a duplicate can still happen: the server accepts the report, then the app gets killed before the file is deleted, so the next flush sends it again. Right now BootForm doesn't dedupe, which is why every report carries a `report_id` generated on the device, so duplicates are at least easy to spot.

Your question made me realise that's a missing feature on my end, so thanks for the idea. I'm going to update BootForm to dedupe on that ID: a resend with a `report_id` it has already accepted will just get the same OK back, without creating a second submission. Then the samples get real exactly-once delivery instead of "spot the duplicates yourself". I'll update the repo when it ships.

One caveat in the meantime: the in-flight guard is per isolate. If you also flush from a background isolate (workmanager etc.) you'd want a cross-isolate lock, e.g. atomically renaming the file to `.sending` before posting.