Mobile apps · Flutter

Docs › Mobile apps

Flutter

iOS, Android, macOS and the web: wrap the app, tap a widget, and the note has its file and line.

People tap a widget in your running Flutter app and write what should change, and the note reaches your coding agent (Claude Code, Codex, Cursor and others) over MCP through a Notato server. Each note has a screenshot with the widget outlined and numbered, a crop of it, its text and key, the app's recent errors, and exactly where it is written: the widget you wrote (Text('£${product.price}')), the file, line and column (lib/main.dart:95:11), and your widgets around it (SpudShop › ShopScreen › ProductCard).

It is the same client as the Swift, Android and .NET MAUI SDKs: the toolbar and its ⋯ menu, the three modes, threads and replies, People only and asides, revert, an offline queue, masking, and a runtime API. Variants are web-only.

Flutter 3.38.1 and later, on iOS, Android and macOS. On the web, notes are kept in memory and a package cannot be shared, only uploaded.

Screenshot size. New captures prefer WebP at quality 85 when it is smaller than PNG, after privacy masking and at the same dimensions. PNG remains a fallback, and existing queues and bundles stay readable. Run flutter pub get and rebuild to include the encoder; unsupported platforms keep PNG. See screenshot storage.

Set up#

flutter pub add notato
npx notato               # the server, the board and MCP for your agent

Wrap your app in it, in main.dart:

import 'package:notato/notato.dart';

void main() {
  runApp(const Notato(project: 'shop', appName: 'Shop', child: MyApp()));
}

And let it know the screens, so notes are filed under the route they were made on (a dialog, or a page pushed without a name, keeps the name of the screen under it):

MaterialApp(navigatorObservers: [Notato.navigatorObserver], ...)

A small dark toolbar appears in the corner. Drag it anywhere; it stays where you leave it, and its chevron folds it into a round button. Tap Annotate, tap a widget, write the note, and press Send. A numbered pin marks it, and turns amber when the agent is on it and green when it is resolved; tap a pin for the note's card. Then ask your agent to "watch Notato and fix what comes in". Everything else is in the toolbar's ⋯ sheet: pins, the notes list, settings, hiding the toolbar or turning Notato off, and the server's state, with Retry when it cannot be reached.

Notes not sent yet, test-mode notes, their screenshots and people's choices (on or off, the toolbar's place, a name) are kept in the app's support folder (path_provider), so they survive a restart; on Android, in the no-backup folder beside it, so a tester's screenshots stay out of Google's backups. On iOS and macOS the support folder is in the device's backups, and Dart cannot mark a folder to be left out: pass a storage that writes elsewhere to keep them out. A package is shared through the share sheet (share_plus).

Reaching the server. The default is http://localhost:4747. The iOS simulator and desktop apps reach it as it is. The Android emulator, and a phone plugged in over USB, reach it once the port is forwarded:

adb reverse tcp:4747 tcp:4747

A phone on Wi-Fi needs the server's dev tunnel: pass its URL as server, or from the build (--dart-define=NOTATO_SERVER=…, below), and the device token as token, in debug builds only. The device token opens notato dev on your machine to whoever has it, and whatever a build is given with --dart-define can be read out of it: give it to flutter run from a file your repository ignores, never to a build you hand out, and never commit it.

Notato(project: 'shop', token: const String.fromEnvironment('NOTATO_TOKEN'), child: MyApp())
flutter run --dart-define-from-file=notato.local.json   # {"NOTATO_SERVER": "https://…devtunnels.ms", "NOTATO_TOKEN": "notato_device_…"}

Notato sends a token only from a debug build unless you set allowTokenInRelease, and never the device token, or any token to a dev tunnel, from a profile or release build, whatever you set. A sandboxed macOS app needs the com.apple.security.network.client entitlement in its debug profile, as the example has.

Builds for testers. A build you hand out (TestFlight, an internal track, a device farm) holds whatever is in it, and anyone with it can read that out. Give it a shared server (notato serve) and a capture-only project token, and set allowTokenInRelease:

npx notato token create shop --purpose app --capture-only --expires 30

A capture-only token can send notes and nothing else: one read out of the app cannot read, change or delete anyone's feedback. Notato asks the server what the token may do before it connects, then sends notes (and those kept on the device) without following the project; replies, status and revert are on the board, and a note's card says so. A test-mode package uploads its captured notes to the configured server too; it grants no access to existing feedback.

Options#

OptionDefault
project(required)Project id on the server. Letters, digits and . _ - @, not only dots
modeNotatoMode.devdev, test or agent (see below)
serverhttp://localhost:4747 (none in test mode)'' for no server: notes stay on the device
tokenA project token (notato_…) for a shared notato serve. Debug builds only, unless allowTokenInRelease
allowTokenInReleasefalseSend token from profile and release builds: a capture-only token only, never a device token
routethe top named route navigatorObserver seesA function that says the screen: notes are filed under it, and its pins shown on it
enableddebug buildsWhether Notato is on at launch. The app can switch it at runtime. Picking a widget needs a debug build either way
showToolbar, toolbarPositiontrue, bottomRightThe toolbar and the corner it starts in. Where people drag it is remembered
authorThe name on this person's notes. They can change it in the settings
screenshotstrueWith a server set (test mode's too), none until it says it takes them; its "off" wins either way
maskInputson in test and agent modeCover text fields in screenshots and leave their values out of notes (password fields always are)
rememberRuntimeStatetrueKeep runtime choices across launches
captureLogs, logLimittrue, 50Attach the app's recent errors (FlutterError, uncaught ones) and debugPrint messages
appName, appVersionFlutter appRecorded on every note
maxScreenshotScale2Pixels per point screenshots are kept at. Phones are 2x to 3.5x; 2x is plenty
storagethe app's support folderWhere notes and choices are kept: (_) async => MemoryStorage() keeps them for this run only

In a release build Notato is your app and nothing else, unless enabled or NOTATO_ENABLED is true when it mounts (that is decided once, so the app is never built again from scratch because Notato came or went). A Notato that takes the place of another (a parent rebuilt it with a new key) leaves Notato running.

From the build#

Two declarations win over the options in code, so a build can switch Notato or send its notes elsewhere without a change to the app. Give them with --dart-define or --dart-define-from-file:

A blank value, or any other, is ignored, and the option in code stands. A release build that does not declare NOTATO_ENABLED decides from enabled alone, as before.

flutter run --dart-define=NOTATO_ENABLED=false   # off, even in debug
flutter run --profile --dart-define=NOTATO_ENABLED=true --dart-define=NOTATO_SERVER=https://…   # on in a profile build

At runtime: notato#

notato (a Listenable)Listen to it for its state: isEnabled, isToolbarVisible, isAnnotating, mode, connection, agents, notes, pendingCount
enable(), disable(), setEnabled(…)On and off. A choice made here wins over enabled until resetRuntimeState()
showToolbar(), hideToolbar()The toolbar. Off still lets the app drive Notato from code
startAnnotating(), stopAnnotating()The next tap selects what is under it
select(key), select('#save')Select a widget (a GlobalKey, an Element, or a selector) as if it had been tapped, and open the note for it
annotate('#save', comment, …)Make a note with no UI, as a person or (with agentName) an agent. peopleOnly: true keeps a person's note from the agent
packageNotes(upload: …)Test mode: the device's notes as a bundle zip
recordRequest(…)Add an HTTP request to the network context of later notes
ListenableBuilder(
  listenable: notato,
  builder: (context, _) => Switch(value: notato.isEnabled, onChanged: (on) => notato.setEnabled(on)),
)

The example's Feedback card switches Notato and its toolbar, and makes a note from code.

What the agent gets#

A tap hit-tests your app's render tree, as Flutter's own widget inspector does, and follows the render object it lands on back to the widgets that made it. Parent in the composer steps out to the widget around it. The note says:

What is never recorded#

NotatoMask(child: Text(user.email))                                         // private: this and everything in it
NotatoMask(private: false, child: TextField(decoration: InputDecoration(hintText: 'Search')))   // recorded even with maskInputs

Password fields stay masked whatever the mark, and a private mark around a private: false one wins over it. NotatoMask paints its child and nothing else. It is the web SDK's data-notato-mask.

Selectors#

What a note's selector looks like, and what an agent passes to notato_annotate or notato.annotate:

#saveThe widget's key (ValueKey('save'), ValueKey(42), ValueKey(Tab.home)), or its Semantics identifier
Text, buttonThe widget's type, its role (button, textbox, switch, img…), or your widget class; * for any
[label="Pay now"]The semantics label, exactly
:text("Add to cart")The text or label contains this, ignoring case
:nth(2)The second match, in painting order
ProductCard > …Leading names are your widgets around it; one the app does not have (a screen's name) is ignored

Modes#

People only and asides#

Replies reach the agent within its active feedback scope. Dev feedback is private to its creator by default; Test feedback is Shared. Change a note's visibility in the board to share it with the project. People only and Aside are additional ways to keep feedback between people:

Known limits#

Hiding completed annotations#

On a resolved or reverted note, open More options → Hide from this app to hide its pin and card on this device. In Settings, Hide completed annotations hides completed notes across every screen at once, including notes beyond the 150 drawn pins. Restore hidden annotations brings them back.

Hiding keeps the feedback, screenshots and thread in the dashboard and exports. It never changes the note on the server. Reopened tasks and new agent questions remain visible. The preference is scoped to the server, project, mode, credential and name, and survives app restarts when runtime preferences are remembered.