<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shahryar Ahmad</title>
    <description>The latest articles on DEV Community by Shahryar Ahmad (@shahryarahmad).</description>
    <link>https://dev.to/shahryarahmad</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078481%2F36255ec1-767e-44fa-a204-7a47728c5238.png</url>
      <title>DEV Community: Shahryar Ahmad</title>
      <link>https://dev.to/shahryarahmad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shahryarahmad"/>
    <language>en</language>
    <item>
      <title>What a Flutter app actually costs to build</title>
      <dc:creator>Shahryar Ahmad</dc:creator>
      <pubDate>Sat, 15 Aug 2026 05:30:58 +0000</pubDate>
      <link>https://dev.to/shahryarahmad/what-a-flutter-app-actually-costs-to-build-11ai</link>
      <guid>https://dev.to/shahryarahmad/what-a-flutter-app-actually-costs-to-build-11ai</guid>
      <description>&lt;p&gt;Almost every page answering this question gives you a table: simple app $20,000 to $50,000, complex app $150,000 and up. The table is useless, because the number it produces is engineering weeks multiplied by a rate, and the ranges are wide enough to contain every possible answer. What is worth knowing is which parts of an app consume the weeks, because that is the part you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost is weeks times rate, and only one of them is about Flutter
&lt;/h2&gt;

&lt;p&gt;A quote is the number of engineering weeks a build takes multiplied by what an engineer costs. Flutter affects the first number, because one codebase produces both platforms. It does not affect the second, which is set by who you hire and where they are, and which varies by a factor of five or more across the market for the same work.&lt;/p&gt;

&lt;p&gt;This is why published cost tables disagree so violently. A page written by a European agency and a page written by an offshore studio can describe the same app and land four times apart, and both can be telling the truth about their own costs. Neither tells you anything about your app.&lt;/p&gt;

&lt;p&gt;So the useful question is not what an app costs. It is how many weeks of work your app contains, and where those weeks go. Get that right and you can price it against any rate you are quoted, and you can tell an inflated estimate from an honest one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Flutter genuinely saves, and what it does not
&lt;/h2&gt;

&lt;p&gt;The saving is real but narrower than it is sold as: one codebase means UI, business logic and state are written once instead of twice. It does not make the platform-specific work disappear, and on most apps that work is a meaningful fraction of the total.&lt;/p&gt;

&lt;p&gt;Written once: screens, navigation, state management, networking, local storage, and the great majority of feature logic. Against two native codebases this is the bulk of a build, and it is where the cross-platform argument is won.&lt;/p&gt;

&lt;p&gt;Still done twice, or done once but per-platform: push notification setup, in-app purchase configuration on both stores, deep links, permissions dialogue copy, background execution, and anything touching a native SDK without a maintained plugin. Signing, provisioning and store submission are also per-platform and are not shared.&lt;/p&gt;

&lt;p&gt;A reasonable planning assumption is that Flutter removes most but not all of the second platform's cost. Treating it as a straight fifty per cent saving overstates it, and any estimate built on that assumption will run over on the release work specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things that actually move the number
&lt;/h2&gt;

&lt;p&gt;In practice, five decisions account for most of the variance between a cheap build and an expensive one: whether there are user accounts, whether there is a backend, whether money changes hands, whether anything happens in real time, and whether the app has to work offline.&lt;/p&gt;

&lt;p&gt;Accounts. Sign-in, password reset, session handling, and — because both stores now require it — in-app account deletion that genuinely deletes rather than deactivates. The deletion path is cheap when you build it alongside sign-in and expensive when you retro-fit it into a schema that assumed accounts were permanent.&lt;/p&gt;

&lt;p&gt;A backend. If your app needs data that is not on the device, someone is building and running a server, and that cost sits outside the app estimate entirely. Apps that can be answered by a device-local database and a third-party API are dramatically cheaper than apps that cannot.&lt;/p&gt;

&lt;p&gt;Payments. A subscription or purchase means store billing on both platforms, receipt validation, restore-purchases, and a paywall that survives review. This is a well-trodden path with good tooling, but it is never a one-week item, and treating it as one is a common source of overrun.&lt;/p&gt;

&lt;p&gt;Real time. Chat, presence, live location or anything that pushes rather than polls brings sockets, reconnection logic, and a whole class of state bugs that only appear on flaky mobile networks. It is the single largest step change in this list.&lt;/p&gt;

&lt;p&gt;Offline. Working without a connection means a local source of truth, a sync strategy and a conflict rule. It is often requested casually and it is never cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line items estimates leave out
&lt;/h2&gt;

&lt;p&gt;Most overruns are not the features. They are the work that everyone knows about and nobody puts in the estimate: release, review, devices, and the weeks after launch.&lt;/p&gt;

&lt;p&gt;Release is its own body of work. Store listings, screenshots, privacy declarations, signing, and the review cycle itself. App Review rejections cluster around a small and stable set of guidelines — external payment steering, missing account deletion, paywalls without terms links, and duplicate-app findings for studios shipping several apps from one codebase. Every one of them is decided by design choices made weeks earlier, which is why they are cheap to design around and expensive to discover at submission.&lt;/p&gt;

&lt;p&gt;Devices. Android fragmentation is real, and so is the range of iPhone sizes still in use. Testing across it is a line item, not a rounding error.&lt;/p&gt;

&lt;p&gt;The month after launch. Crash reports arrive from hardware you did not test on, and the first real users find the paths your test data never covered. Budget for it explicitly; an estimate that ends at submission is describing something other than a shipped product.&lt;/p&gt;

&lt;p&gt;Maintenance after that. Both platforms ship an OS version every year, each with SDK requirements that eventually become mandatory. An app nobody maintains does not stay still — it stops being installable.&lt;/p&gt;

&lt;h2&gt;
  
  
  One engineer or an agency, and where each actually wins
&lt;/h2&gt;

&lt;p&gt;The saving from hiring one engineer is not the rate. It is the rework you do not pay for later, and the absence of a translation layer between the person deciding and the person typing. The agency wins on scale, and the honest answer depends on which of those your project needs.&lt;/p&gt;

&lt;p&gt;Most of what I get called in to fix is an app that was built cheaply, shipped, and then cost more to stabilise than it did to write in the first place — memory leaks, no state management layer, an API called four times where once would do. On one such engagement, refactoring to Clean Architecture with BLoC rather than patching symptoms cut unnecessary API calls by around 40% and improved performance and stability by around 70%, in four weeks. That is four weeks that would not have been spent if the architecture had been right the first time, and it is the real cost of the cheap bid.&lt;/p&gt;

&lt;p&gt;Where an agency genuinely wins is scale: several engineers in parallel, a project manager, a design team. If that is the shape of your project, hire the agency — one senior engineer is the wrong tool for it, and I will say so on the call.&lt;/p&gt;

&lt;p&gt;For a single well-scoped product, though, the ownership is the point. In 2026 I built and released five iOS apps as the only engineer, each one taken from an empty repository through App Review to the store, so I am describing that scope from having done it rather than from having estimated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read a quote you have been given
&lt;/h2&gt;

&lt;p&gt;Ask for the estimate broken into engineering weeks per area rather than one total, and check that release, testing and the first month after launch appear as their own lines. An estimate that cannot be decomposed is a guess with a decimal point on it.&lt;/p&gt;

&lt;p&gt;Then check the five drivers above against what is written down. If the app has accounts, payments and real-time features but the estimate is flat across them, whoever wrote it has not thought about it — those areas do not cost the same and an estimate that says they do is not an estimate.&lt;/p&gt;

&lt;p&gt;Ask what happens when a scope change arrives mid-build, because one will. The answer tells you more about what the engagement will actually cost than the headline figure does.&lt;/p&gt;

&lt;p&gt;I quote per engagement rather than publishing a single hourly figure, because the rate genuinely depends on duration and scope. What I will do on a first call is decompose your app into the areas above and tell you which ones are expensive, whether or not you end up working with me.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://shahryarahmad.com/blog/flutter-app-development-cost" rel="noopener noreferrer"&gt;shahryarahmad.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>startup</category>
      <category>career</category>
    </item>
    <item>
      <title>Where redundant API calls come from in a production Flutter app</title>
      <dc:creator>Shahryar Ahmad</dc:creator>
      <pubDate>Sat, 15 Aug 2026 05:27:52 +0000</pubDate>
      <link>https://dev.to/shahryarahmad/where-redundant-api-calls-come-from-in-a-production-flutter-app-a7e</link>
      <guid>https://dev.to/shahryarahmad/where-redundant-api-calls-come-from-in-a-production-flutter-app-a7e</guid>
      <description>&lt;p&gt;On an app that already has users, cutting unnecessary network traffic by around forty per cent did not take a rewrite. Nearly all of it came from three patterns: fetches triggered by rebuilds, screens that reload their entire state on every navigation, and identical requests firing concurrently because nothing deduplicated them.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, measure — the guesses are usually wrong
&lt;/h2&gt;

&lt;p&gt;Before changing anything, log every outbound request with its endpoint, a timestamp and the widget or bloc that triggered it. Ten minutes of ordinary use produces a list that is almost always surprising, and it stops you optimising an endpoint that fires twice a session while ignoring one firing on every frame.&lt;/p&gt;

&lt;p&gt;An interceptor on your HTTP client is enough. You are looking for two things: the same URL appearing repeatedly in a short window, and requests firing during interactions that should not need the network at all — scrolling, opening a sheet, rotating the device.&lt;/p&gt;

&lt;p&gt;Keep that log. It is also how you prove the change worked, which matters when the improvement is invisible in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source one: fetching inside build
&lt;/h2&gt;

&lt;p&gt;Anything that starts a request from &lt;code&gt;build()&lt;/code&gt; — directly, or through a provider constructed inline — will refetch every time the widget rebuilds, and widgets rebuild far more often than most people assume. This is the largest single source of redundant traffic I have found in production apps.&lt;/p&gt;

&lt;p&gt;The tell is a request that fires when nothing about the data changed: a keyboard opening, a parent rebuilding, a theme change, an animation frame. Because each individual call is cheap, this hides until you look at the log.&lt;/p&gt;

&lt;p&gt;The fix is to move the trigger out of the render path — into &lt;code&gt;initState&lt;/code&gt;, a bloc event, or a provider that caches its own result — so the fetch is tied to an intent rather than to a repaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source two: navigation that discards state
&lt;/h2&gt;

&lt;p&gt;If pushing a detail screen and popping back refetches the list, the list state is being rebuilt rather than retained. On a slow connection this is what users describe as the app feeling heavy — a spinner where they expected the screen they just left.&lt;/p&gt;

&lt;p&gt;Hold list state above the navigation boundary, so returning to a screen restores what was already loaded. In BLoC terms, the bloc outlives the route rather than being created by it.&lt;/p&gt;

&lt;p&gt;Then make refresh explicit: pull-to-refresh, or an event when something is known to have changed. Deliberate refresh is both cheaper and more predictable than refetch-on-every-appearance, and users understand it better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source three: concurrent duplicate requests
&lt;/h2&gt;

&lt;p&gt;Two widgets asking for the same resource at the same moment produce two identical in-flight requests. Keep a map of in-flight futures keyed by request, return the existing future when one is already running, and the second caller waits on the first instead of duplicating it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;{};&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_inFlight&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whenComplete&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_inFlight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="n"&gt;_inFlight&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a small amount of code that removes an entire category of waste, and it also removes a class of race condition where two responses for the same resource arrive out of order and the older one wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache with an explicit lifetime, not forever
&lt;/h2&gt;

&lt;p&gt;Add a short time-to-live to responses that do not change second by second, and serve from cache within that window. Even thirty seconds eliminates most repeat traffic during a single session, and unlike an unbounded cache it cannot leave users staring at stale data.&lt;/p&gt;

&lt;p&gt;Decide the lifetime per endpoint rather than globally: a currency rate and a user profile do not have the same tolerance for staleness.&lt;/p&gt;

&lt;p&gt;Pair it with cache invalidation on the events that genuinely change the data — a successful write should clear what it affects, rather than waiting for a timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debounce anything driven by typing
&lt;/h2&gt;

&lt;p&gt;Search fields wired straight to a request send one per keystroke. Debouncing by a few hundred milliseconds turns a nine-request word into one, and cancelling the previous request when a new one supersedes it stops out-of-order responses overwriting the current results.&lt;/p&gt;

&lt;p&gt;Cancellation matters as much as debouncing here. Without it, a slow response to "rec" can land after the fast response to "receipt" and repopulate the list with the wrong results — a bug that is maddening to reproduce and obvious once you see the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this adds up to
&lt;/h2&gt;

&lt;p&gt;None of these are clever. They are ordinary discipline about when a request is allowed to happen, applied consistently. The effect on a real app is a large reduction in traffic, a proportional reduction in backend cost, and an app that feels faster without a single change to how it looks.&lt;/p&gt;

&lt;p&gt;The reason to do the measurement first is that it tells you which of the three sources dominates in your app, and they are rarely equal. On the apps I have worked on, fetching from &lt;code&gt;build()&lt;/code&gt; has been the biggest by some distance — and the cheapest to fix once you can see it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://shahryarahmad.com/blog/flutter-reducing-api-calls" rel="noopener noreferrer"&gt;shahryarahmad.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>performance</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Getting a Flutter app through App Review</title>
      <dc:creator>Shahryar Ahmad</dc:creator>
      <pubDate>Sat, 15 Aug 2026 05:17:02 +0000</pubDate>
      <link>https://dev.to/shahryarahmad/getting-a-flutter-app-through-app-review-3did</link>
      <guid>https://dev.to/shahryarahmad/getting-a-flutter-app-through-app-review-3did</guid>
      <description>&lt;p&gt;Most App Review rejections are not surprises. They come from a small, stable set of guidelines, and nearly all of them are decided by choices you make weeks before you submit — how you gate the paywall, whether an account is required, how you word a subscription screen. Fixing them at submission time is expensive. Designing around them costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3.1.1 — anything unlockable has to go through in-app purchase
&lt;/h2&gt;

&lt;p&gt;If your app unlocks digital content or features, the purchase runs through StoreKit. Not Stripe, not a web checkout you deep-link to, and not a discreet link to your website pricing page. This is the single most common rejection I see on apps that already work perfectly.&lt;/p&gt;

&lt;p&gt;The failure mode is rarely a deliberate attempt to dodge the commission. It is usually a team that already had a web product with a payment provider wired up, and reused it. Review notices, because reviewers open the paywall.&lt;/p&gt;

&lt;p&gt;The part teams miss more often is subtler: you may not steer users toward an external purchase either. A line reading "manage your plan on our website" next to a locked feature is enough to draw a rejection, even when the in-app purchase exists and works.&lt;/p&gt;

&lt;h2&gt;
  
  
  5.1.1(v) — if you can create an account, you must be able to delete it
&lt;/h2&gt;

&lt;p&gt;Any app that supports account creation must offer account deletion from inside the app, and it must actually delete rather than deactivate. A support email address is not compliant. This one is binary and it is checked.&lt;/p&gt;

&lt;p&gt;Build it as a real path: a settings entry, a confirmation, a server-side delete, then sign-out. On a Supabase or Firebase backend, remember that deleting the auth record is not the same as deleting the rows keyed to it. Reviewers do not inspect your database, but users notice, and a half-delete is a data-protection problem regardless of what Review saw.&lt;/p&gt;

&lt;p&gt;The cheapest time to build this is when you build sign-in. Retro-fitting deletion into a schema that assumed accounts were permanent is where it turns into a week of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  4.3 — spam, and why it hits template-built apps hardest
&lt;/h2&gt;

&lt;p&gt;Guideline 4.3 covers apps that duplicate something already on the store, including your own other apps. Ship five apps from one codebase with different colours and names and you are a candidate, regardless of how different they feel to you.&lt;/p&gt;

&lt;p&gt;This matters disproportionately for Flutter studios, because sharing a design system and a networking layer across apps is exactly what Flutter is good at. The reuse is legitimate; the problem is when reuse extends to the app's actual purpose.&lt;/p&gt;

&lt;p&gt;What survives review is genuine differentiation in what the app does, not in its palette. If two of your apps could be one app with a toggle, Review may well conclude that they should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  3.1.2 — subscription screens are read closely
&lt;/h2&gt;

&lt;p&gt;A subscription paywall must state, on the screen where the user commits, what they are buying, the price, the billing period, and links to your terms and privacy policy. Missing terms links are one of the easiest rejections to earn and one of the easiest to avoid.&lt;/p&gt;

&lt;p&gt;Put those links in the paywall itself. Not in settings, not in the App Store description, not two navigations away. Reviewers look at the purchase screen.&lt;/p&gt;

&lt;p&gt;Free trials attract particular attention: if there is a trial, say plainly how long it lasts and what happens when it ends. Wording that leaves a reader unsure whether they are about to be charged is treated as unclear rather than clever.&lt;/p&gt;

&lt;h2&gt;
  
  
  2.1 — the reviewer must be able to get in
&lt;/h2&gt;

&lt;p&gt;If your app requires sign-in, supply working demo credentials, and check them the day you submit. A large share of "we need more information" responses are simply a reviewer unable to reach the thing they are meant to review.&lt;/p&gt;

&lt;p&gt;Add notes explaining anything non-obvious: which flow to follow to see the core feature, whether a backend needs to be live, what the app does with a permission it requests. A reviewer with ten minutes and no context will not discover a feature buried three screens deep.&lt;/p&gt;

&lt;p&gt;For anything that depends on a server, make sure it is up during review, and remember that review can happen at any hour in any timezone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions: ask late, and explain why in your own words
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;NSCamera&lt;/code&gt;, &lt;code&gt;NSMicrophone&lt;/code&gt;, &lt;code&gt;NSPhotoLibrary&lt;/code&gt; and location usage string is read. Generic strings — "we need camera access" — are a common rejection, and they are also bad product design: a permission prompt with no reason attached is the highest-friction moment in a first session.&lt;/p&gt;

&lt;p&gt;Write the string as a sentence explaining what the user gets: what the app will do with the camera, in this app, for this feature.&lt;/p&gt;

&lt;p&gt;Then ask at the moment the feature is used rather than at launch. Acceptance rates are dramatically better when the request follows an action the user just took, and it removes an entire class of rejection at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually reduces review cycles
&lt;/h2&gt;

&lt;p&gt;Treat the guidelines as design constraints during the build, not a checklist at the end. Every item above is a decision that costs nothing when made early and costs a resubmission cycle — typically several days each — when made late.&lt;/p&gt;

&lt;p&gt;The practical routine: build account deletion alongside sign-in; write permission strings when you add the permission; put terms and price on the paywall the first time you build it; and check demo credentials on submission day, every time.&lt;/p&gt;

&lt;p&gt;The apps I have taken through review most smoothly were not the ones with the best submission notes. They were the ones where none of this was left to the end.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://shahryarahmad.com/blog/flutter-app-store-rejection" rel="noopener noreferrer"&gt;shahryarahmad.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ios</category>
      <category>mobile</category>
      <category>appstore</category>
    </item>
  </channel>
</rss>
