TLDR: I redesigned a live customer app one screen at a time. Claude Design decided the layout. Cursor wrote the Flutter. I checked every screen on a real phone, with the Android navigation bar and the iOS home indicator turned on, and I read the diff before keeping it. Replacing the old icon set and deleting what nothing referenced took about 1.9 MB out of the app. The agent is fast. It does not know when the result is wrong.
A visual revamp of an app people already use is not one prompt. I thought it might be. It is not. This app had bookings, payments, chat, and a handful of service funnels. Customers were in it every day. I needed the product to look new without breaking the flows under the paint.
What actually worked was a short loop. Design the screen. Hand Cursor a small slice. Open it on a phone. If it is wrong, send a screenshot and one sentence. Repeat. I started calling that loop the agentic development life cycle, mostly so I would stop skipping steps.
Decide the screen before you open the editor
I designed the screens in Claude Design first. Title, primary button, what belongs in a sheet, what stays on the page. By the time Cursor saw the work, the layout was already chosen.
Then I dropped the design into the chat as an image and named the screen in the app. The prompt that worked was boring and specific: match this, keep the existing data and navigation, do not restyle the rest of the app.
“Make this nicer” was a waste. The agent will invent a layout, and you will spend the next hour deleting it.
When the build missed the design, I did not rewrite the brief. I sent a photo of the phone and said what was wrong. The payment summary is under the nav bar. The button is touching the system bar. This sheet has a gap and the previous screen is showing through. Cursor is good at a bug you can see. It is bad at guessing which layout you had in your head.
Icons are part of the design, not a cleanup task
The new screens needed one icon style. I treated that as design work, then as a code change, in that order.
I exported each icon as an SVG at the size the UI actually uses, and I gave it a name I would still understand in six months. One file per icon. I swapped the widget over to the new file and deleted the old one in the same change.
SvgPicture.asset(
'assets/images/funnels/icon_home.svg',
width: 20,
height: 20,
)
That is the whole integration. The design lives in the file. The widget only points at it. If I later changed the glyph, I replaced the SVG and left the Dart alone.
A glyph that looks crisp in a design tool often goes soft at 24 pixels on a phone, so I checked the small size on a light background before moving on. I also stopped keeping the old SVGs “just in case.” If two icon sets are still in the project, two screens will keep using two different languages, and you will not notice until a customer does.
I only deleted an asset after nothing referenced it. Deleting first, then finding an old funnel that still points at the file, is a crash you caused yourself.
What actually got faster
I did not keep DevTools traces, so I will not invent a frame-time percentage. The number I can defend comes from the asset bundle, counted from git before and after the revamp.
| Before | After | |
|---|---|---|
| SVG files | 225 | 136 |
| SVG size | 3.49 MB | 1.52 MB |
| Bundled images (SVG, PNG, and the rest) | 342 files, 11.57 MB | 252 files, 9.65 MB |
The single heaviest file that left was the old home banner, 346 KB of SVG. A few training illustrations were in the 50–70 KB range each, drawn at a size the screen never displayed. The replacements are small because they were exported at the size the UI uses.
That is install size and decode cost, which is the part customers feel on a slow network and on a cheap phone. It is also the part you can measure without a profiler: git knows the blob size of every deleted file.
Two limits I did set in code, because an unbounded wait is its own performance bug.
Startup now calls validate_token before the first screen, and that call is capped at 5 seconds. If the socket is still open when the timer fires, the client is closed and the app opens anyway. A stalled request cannot hold a blank window open past that.
Search results stay on a ListView.builder. A redesigned card is still one child of a lazy list. Rebuilding the row did not turn the list into a column of every partner in the city.
Keep the agent inside one screen
This is the part that saves you. Cursor will keep going. A padding fix turns into a shared widget, then ten call sites, then a quiet change in how payment or login works.
So the prompt had a fence around it. Which screen. What done looks like, usually the picture. What not to touch. Auth, payments, and any copy that support or legal owns were called out when the file sat next to them. If overflow, color, and safe area were three different bugs, they were three different turns.
After each slice I read the diff. If a file I did not mention had changed, I asked why before accepting it. That review caught more than another round of prompting.
The bugs that are not in the design
A design frame does not include the Android three-button bar or the iOS home indicator. Those were the bugs that came back after a screen already “looked done.”
SafeArea was not enough. On some Android phones the system reports no bottom inset while the navigation bar is still painted over your list. Twenty-four pixels of padding, and the last card slides underneath. I ended up with one helper that reads the real inset and, only when the phone reports nothing, uses a small fallback. Buttons and the last row of a list use it.
double systemBottomInset(BuildContext context) {
final mq = MediaQuery.of(context);
final inset = math.max(
math.max(mq.viewPadding.bottom, mq.padding.bottom),
mq.systemGestureInsets.bottom,
);
if (inset > 0) return inset;
if (!kIsWeb && Platform.isAndroid) return 48;
if (!kIsWeb && Platform.isIOS) return 34;
return 0;
}
The bar then adds that inset under its own padding, so the button sits above the system chrome and the white background still paints to the edge of the screen.
padding: EdgeInsets.fromLTRB(
padding.left,
padding.top,
padding.right,
padding.bottom + systemBottomInset(context),
),
Sheets are a different problem. The white sheet should reach the bottom of the screen. The inset belongs inside the list, or under the button. If you pad the whole sheet up, you get a clear strip and the page behind shows through. I shipped that bug, saw it, and fixed it.
Then I checked iOS. A fix that adds “the inset, plus 12 pixels” on Android can double-pad an iPhone that already reports a home indicator. Same screen, both phones, before I called it done.
Anything that sits on the last pixel got the same treatment. Reschedule, address, payment summary, search results. If the design shows a button near the bottom, turn the system bar on. The emulator chrome will lie to you.
How I tested
I stopped treating “the agent says it should work” as a test.
I put the design and the phone next to each other and looked at hierarchy, spacing, and the main button. A desktop screenshot of a phone frame was not enough.
Cases were written with an iOS column and an Android column, because the platforms fail differently. OTP autofill is the example I keep using. On Android the SMS can contain the phone number and the code. The field has to take the code. A visual comp will never tell you that.
/// A standalone 4-digit code, not digits inside a phone number.
static const String _otpRegex = r'(?<!\d)\d{4}(?!\d)';
listenForCode(smsCodeRegexPattern: _otpRegex);
A message like “your code is 4821, sent to 9876543210” used to fill the field with the phone number. The lookbehind is what stopped that.
I also walked the states, not just the happy path. Empty, cancelled booking, payment due, keyboard open, system navigation on. The cancelled layout and the payment-summary card were separate bugs from the default screen.
Pure rules went into unit tests. Which service type is selected does not need a phone. Whether a card hides under a nav bar does. I stopped pretending one replaces the other.
Later I added a startup check that validates a stored login token and gives up after a few seconds. That got its own cases. Valid token, expired token, offline, timeout, server error. A 500 must not log the user out. A hung request must not leave the app on a blank screen. The timed-out request has to be cancelled, or a late response can log someone out after the app has already opened.
Future<void> _validateStoredToken() async {
final token = Pref.instance.data.getString(kDataToken) ?? '';
if (token.isEmpty) return;
try {
await ApiCaller.get(
kUrlValidateToken,
withToken: true,
timeout: const Duration(seconds: 5),
);
} catch (error, stack) {
debugPrint('Token validation skipped: $error');
debugPrint(stack.toString());
}
}
The timeout is not Future.timeout on a shared client. That only stops waiting. The request keeps running. This call uses its own client and closes it when the 5 seconds are up, which aborts the socket.
final client = http.Client();
return client.get(uri, headers: headers).timeout(timeout, onTimeout: () {
client.close();
throw TimeoutException('Request timed out', timeout);
}).whenComplete(client.close);
A 401 still clears the session. If the navigator is not up yet, which it is not during this startup check, the token is removed directly and the app opens the welcome screen because nothing is left to log in with.
if (navContext != null) {
authLogout(navContext);
} else {
await _clearStoredSession();
}
What I would not hand over
I did not paste secrets, keystores, or environment passwords into the chat so the agent could “wire up the build.”
I did not ask it to update every screen in one turn. You will not review that diff, and you will ship a regression in a funnel you never opened.
I did not let a padding task change payment or auth. Those are different risks. Say so in the prompt.
And I did not sign off from a screenshot if I could not finish the booking. If the flow is broken, the screen is not done.
How to use ADLC on a real screen
Here is the loop on one screen, the booking review page, in the order I actually did it. Copy this onto the next screen. Change the file name and the picture. Leave the order alone.
Before you open Cursor, you need three things on your desk.
The design, already finished in Claude Design. A picture of the screen: title, the Pay Now button, what opens in a sheet, what stays on the page. Export it. If there is a new icon, export that SVG at the size the phone will show, with a file name you will still understand in six months.
The file. For this one it was the booking review screen. Write the path down. If you cannot name the file, you are not ready to prompt.
Two sentences, written by you:
Done: the screen matches the picture, and Pay Now sits above the Android navigation bar. Leave alone: payment, login, and every other service.
That is the whole brief. “Make it nicer” is how you lose an hour.
Then one message in Cursor.
Attach the picture. Paste the two sentences, and name the file:
Match the attached picture on the booking review screen.
Keep the existing data, routes, and copy.
Do not edit payment, login, or any other service.
Done means Pay Now sits above the system navigation bar.
Send it once. Read what comes back before you send anything else.
Then the phone, not the desktop preview.
Install the build. Open booking review.
On Android, turn on the three-button navigation bar. Look at Pay Now. If the button touches that bar, or the payment summary slides under it, the screen is wrong.
On iOS, do the same with the home indicator.
Then tap through the screen the way a customer would. Empty state. Keyboard open. A cancelled booking, if this screen has one. The picture from Claude Design will not show those.
Then the diff.
Open the changed files. You asked for the booking review screen. If only that file changed, keep it. If payment or login changed too, ask why before you accept it. Drop the part that wandered.
If the phone is wrong, send the phone.
Take a screenshot of the actual device. Add one sentence. “The payment summary is under the nav bar.” Send that. Do not rewrite the brief, and do not ask it to redesign the page.
When the new build is on the phone, look at Pay Now again. Repeat until the button clears the bar and you can finish a booking.
Then stop.
Write one line in your notes: booking review, Android, three-button bar on, Pay Now clears the bar, booking completed. Tomorrow, the next screen, same five beats: picture, file, two sentences, phone, diff.
Key takeaways
- Settle the layout in the design tool. Cursor should implement a decision you already made.
- One screen, one turn. A padding fix that edits login is a failed turn.
- When the build is wrong, send the phone photo and one sentence.
- Read the diff. Keep the files you named.
- Design frames leave out the Android navigation bar and the iOS home indicator. Turn both on before you call a bottom button done.
- Put the bottom inset inside the sheet. Padding the whole sheet leaves a gap, and the page behind shows through.
- Swap in the new SVG and delete the old file only after nothing references it. That cut bundled images here from 11.57 MB to 9.65 MB.
- Cap any wait you add. The startup token check gives up at 5 seconds and closes the socket.
- Test the states the comp hides: empty, error, keyboard open, cancelled, payment due. iOS and Android fail in different places.
- Leave secrets out of the chat. A screenshot is not done if you could not finish the booking.
Conclusion
The same revamp, done the old way, would have taken two to three months. Design handoff, then implementation, then a QA pass that found the Android navigation bar after the screens were already called done. With ADLC it reached production in 10 days. The design was settled in Claude Design, Cursor implemented one screen at a time, and each screen was checked on a phone before the next one started.
That 10 days is a delivery number, and the other results are just as concrete.
Delivery covered the screens customers actually open. Vet, grooming, dog walking, training, boarding, adoption, mating, profile, membership, bookings, and chat. Each one went out as a small diff I had read. The booking flow still completed. Payment and login stayed out of the padding tasks.
Quality showed up on the phones, where the design file is silent. Pay Now sits above the Android three-button bar and the iOS home indicator. Sheets reach the bottom of the screen, and the page behind no longer shows through a gap. OTP autofill takes the 4-digit code when the SMS also contains a phone number. A stored login is checked before the first screen, a 401 clears the session, and a 500 or a timeout leaves the user logged in. Those cases were written per platform, and a screenshot was not enough to call a screen done.
Performance is the part I can recompute. Bundled images went from 11.57 MB to 9.65 MB. SVG files went from 225 to 136, and from 3.49 MB to 1.52 MB, because the new icons were exported at the size the UI uses and the old set was deleted once nothing referenced it. Search results stayed on a lazy list. The startup token check gives up after 5 seconds and closes the socket, so a hung request cannot hold a blank screen.
Cursor wrote the Flutter faster than I would have. It did not know when to stop. That part stayed with me, on a phone, with the design still open. The 10 days only count because the screens that shipped still work.
