Let’s be honest: most code reviews feel like a chore. At best, they’re a quick rubber stamp. At worst, they’re a nitpicky, soul-crushing critique of your variable naming choices. But a truly effective flutter code review is neither of those things.
The goal isn’t to find every missing semicolon. The linter can do that. The goal is to elevate the code, share knowledge, and ensure what we’re building is solid, maintainable, and actually solves the user’s problem.
So, how do we get there? It starts with shifting our mindset.
The Mindset: From Gatekeeping to Growth
Before we dive into the nitty-gritty of widget trees and state management, we need to get the philosophy right. A bad review process feels like trying to get your code past a grumpy gatekeeper. A great one feels like a collaborative workshop.
It’s About Intent, Not Just Implementation
The most important question in a code review isn’t “Is this code perfect?” It’s “Does this code successfully achieve its intended goal?” We can get so lost in implementation details that we forget to ask if the change even makes sense for the business or the user. Always start here. A perfectly architected feature that solves the wrong problem is still a failure.
It’s a Learning Opportunity, Not a Test
A pull request is a fantastic, self-contained opportunity for knowledge sharing. The author might know the business context best, while the reviewer might see a clever Dart pattern or a potential performance trap. The goal is for everyone to leave the review knowing a little more than they did before. It’s not about passing or failing; it’s about collective improvement.
It’s About Shared Ownership, Not Blame
Once a PR is merged, it’s not “your code” or “my code” anymore—it’s “our code.” The review process is the moment we formalize that shared ownership. The reviewer isn’t just pointing out flaws; they are accepting joint responsibility for the change. This simple shift in perspective turns an adversarial process into a cooperative one.
What to Look For in a Flutter Code Review
Okay, mindset is set. You’ve got a PR in front of you. What should you actually be looking for? Here’s a breakdown of the key areas, from high-level architecture down to the small details.
Architecture and State Management Patterns
This is the big one. Inconsistency here leads to a tangled mess that’s impossible to debug or build upon.
- Consistency: Does this code follow the established architectural pattern (BLoC, Riverpod, Provider, etc.)? If it deviates, is there a compelling, documented reason why?
- Responsibility: Are widgets just for displaying UI? Is business logic properly separated in blocs, controllers, or notifiers? Are services handling API calls? Each piece should have one clear job.
- State Propagation: Is state being passed down efficiently? Are we using the right tools (like
Provider.of,ref.watch, orcontext.select) to avoid unnecessary rebuilds?
UI Responsiveness and Widget Tree Structure
A beautiful UI on your iPhone 14 Pro Max can easily break on a tiny iPhone SE. Don’t trust the simulator alone.
- Responsiveness: Does the layout work on different screen sizes and orientations? Are widgets like
LayoutBuilder,FractionallySizedBox, or flexible layouts being used correctly? - Widget Tree Depth: Is the widget tree unnecessarily deep? Can complex
buildmethods be broken down into smaller, more manageable widgets? - Use of
const: Is theconstkeyword used wherever possible for widgets that don’t change? This is one of the easiest performance wins in Flutter.
Performance and Memory Usage
Janky animations and high memory usage are silent killers of a good user experience.
- Async Operations: Are
Futures andStreams handled correctly? Are there loading and error states for async UI? Isawaitbeing used ininitStatewithout care? - Expensive Operations: Are there heavy computations happening in the
buildmethod? These should be moved out and cached. - Memory Leaks: Are controllers, subscriptions (like
StreamSubscription), and other resources properly disposed of in thedispose()method? This is a classic source of bugs.
Error Handling and Null Safety
Hope is not a strategy. What happens when the API call fails or that variable is null?
- Graceful Failure: Does the app crash if an API call fails, or does it show a user-friendly error message? Are
try-catchblocks used appropriately? - Null Safety: Is the code truly null-safe, or is it littered with bang operators (
!)? Each!is a potential runtime crash. Question every single one.
Test Coverage and Testability
Code that isn’t tested is broken by default. You just don’t know it yet.
Is there a test? Does the change include a corresponding unit, widget, or integration test?
Meaningful Tests: Do the tests check for actual business logic and UI behavior, or just that the code doesn’t crash?
Testability: Is the code itself easy to test? If not, it’s often a sign of tightly coupled components that should be refactored (e.g., using dependency injection).
Code Style, Readability, and Documentation
This is about being a good citizen. The next person to touch this code (which might be you in six months) will thank you.
- Clarity: Are variable and method names clear and unambiguous?
- Comments: Do comments explain the why, not the what? Good code explains itself; comments should provide context that the code can’t.
- Consistency: Does the code follow the project’s established style guide (e.g., Effective Dart)?
✅ Practical Flutter Code Review Checklist
Use this quick checklist to review a PR efficiently:
Architecture and Organization
Follows the team’s architecture pattern
Clear separation of concerns (UI, logic, data)
Folder and file structure consistent with the project
State and Navigation
Consistent state management (Provider, BLoC, Riverpod, etc.)
No business logic inside Widgets
Navigation handled cleanly (named routes, go_router, auto_route, etc.)
UI/UX
Responsive layout across devices
Uses global theme (ThemeData) correctly
Loading, empty, and error states handled properly
Performance
const used wherever possible
Avoids unnecessary rebuilds or heavy computations
No leftover prints, debug logs, or dead code
Error Handling
Errors handled consistently (try/catch, Either/Result, etc.)
Clear user feedback for failures
No sensitive stack traces or data exposed in logs
Security and Backend Integration
No hardcoded secrets (tokens, keys, private URLs)
Models properly validated and serialized (fromJson/toJson)
Handles timeouts and offline states
Testing and Quality
All tests passing
Code formatted and lint-clean
Descriptive names and meaningful comments