Most Flutter pull requests we review don't fail on clever bugs. They fail on the same dozen things: a ! on a value that's null on slow networks, a BuildContext used after an await, an API key in a Dart file, a platform channel that replies on the wrong thread. This Flutter code review checklist is the one we use internally and when a client asks us to audit a codebase someone else wrote. It's ordered roughly by how expensive each problem is to find later.
Architecture and structure
- Is the code organised by feature?
lib/features/meal_log/with its own data, state and UI ages better than one globalscreens/,models/,services/split. Crumb Count has 13 feature modules across 82,000+ lines of Flutter, and that only stays navigable because features don't reach into each other's internals. - Do widgets talk to data directly? A
FirebaseFirestore.instanceorhttp.getinside a widget'sbuildorinitStateis a flag. Data access belongs in a repository the widget never sees. - Is there one way of doing each thing? Two HTTP clients, two date libraries, two routing approaches. Pick one, note it in the PR.
- Are new dependencies justified? Check the package's last release, open issues, platform support and licence. A package that wraps ten lines of code isn't worth the upgrade risk.
- Are files reasonable in size? A 1,000-line screen widget usually hides three components that want extracting.
State management, rebuilds and async
- Is the state solution used consistently? Whether it's BLoC, Riverpod or
ChangeNotifier, mixing patterns in one feature makes it hard to know where truth lives. (We moved one client app from GetX to BLoC partly for this reason; see our GetX to BLoC migration write-up.) - Is state scoped correctly? Global state for something only one screen uses means unrelated rebuilds and data that outlives the screen.
- Is
BuildContextused after an async gap? Afterawait, checkmounted(orcontext.mounted) before navigating or showing a snackbar. The analyzer'suse_build_context_synchronouslylint catches most of these, so make sure it's on. - Are controllers, streams and subscriptions disposed? Every
AnimationController,TextEditingController,StreamSubscriptionandTimercreated in aStateshould be cleaned up indispose. - Are futures created in
build? AFutureBuilder(future: fetchThing())re-runs the request on every rebuild. Create the future once. - Are loading, empty and error states all handled? Reviewers should ask what the screen shows offline, with zero items, and when the server returns a 500.
- Is
constused where it can be? Cheap, and the linter will tell you.
Null safety and types
- Every
!needs a reason. The bang operator says "I promise this isn't null". Ask what happens when the promise breaks, because a crash in production is usually the answer. - Is
lateused to dodge the type system? Alatefield read before it's assigned throws at runtime. Prefer nullable with a clear check, or initialise in the constructor. - Is JSON parsing typed?
json['user']['name'] as Stringcrashes when the server omits a field. Use model classes with explicit handling of missing or unexpected values, generated or hand-written. - Is
dynamicleaking? Turn on strict analysis options (strict-casts,strict-raw-types) and see what lights up.
Platform code, security and tests in this Flutter code review checklist
Platform channels and native code
- Is the channel contract in one place? A single Dart class per native capability, or Pigeon-generated code, beats method-name strings scattered across files.
- Does native code reply on the main thread? Flutter's docs require channel handlers to run on the platform's main thread. On iOS that means dispatching back to the main queue; on Android, heavy work goes to a background task queue.
- Are native errors returned, not swallowed? The Dart side should get a
PlatformExceptionwith a useful code, and handle it. - Are permissions and entitlements documented? New
Info.plistusage descriptions, Android manifest permissions and background modes need a line in the PR description. App Store review rejects vague usage strings.
Security
- No secrets in the app. Anything in Dart or in the binary can be extracted. Third-party API keys that cost money (LLM providers, maps, SMS) belong behind your server. For Crumb Count's AI meal logging, calls go through a server-side proxy with per-user quotas, and the proxy has its own adversarial test suite covering JWT forgery, key confusion and cost attacks.
- Tokens in secure storage (Keychain or Keystore via a maintained package), not SharedPreferences.
- No sensitive data in logs. Check every
printanddebugPrint; check what crash reporting captures. - Is the server enforcing the rules? Hiding a button for non-admins is UI. Firestore security rules or backend checks are security. A review should confirm both.
- Deep links validated? Treat parameters from a URL like any other untrusted input.
Tests
- Does business logic have unit tests? Repositories, blocs or notifiers, parsers, date and money calculations. These are cheap and catch most regressions.
- Do key widgets have widget tests for loading, error and empty states?
- Are platform channels mocked in tests, with a short manual checklist for device-only behaviour (widgets, background tasks, health data)?
- Does CI run
flutter analyzeandflutter teston every PR? A checklist is only as good as the automation behind it.
Using the checklist without slowing everyone down
Automate everything a linter can catch, and keep human review for structure, security and behaviour. Start with flutter_lints or a stricter set, fail CI on analyzer errors, and let reviewers spend their attention on the questions a tool can't answer: does this belong here, what happens when it fails, and would a new developer understand it in six months.
If you'd like an outside review of a Flutter codebase, whether you've inherited it or you're about to scale the team, orithLabs runs code audits as part of our technical consulting work, and we can set up the test and CI side through QA and testing.