Skip to content

Best Practices

  • Always enable null safety (dart migrate).
  • Avoid usage of dynamic and Object declaration, always use var or explicit typing.
  • Follow the order of const >> final >> var with no nullability specifier >> final late >> var?.
  • Avoid non const top-level variables.
  • Favor immutable data classes (with freezed or equatable).
  • Avoid dynamic where possible; use explicit types (List<int> instead of List).
  • Prefer async/await over .then() for readability.
  • Use Future.error for explicit errors instead of throwing strings.
  • Leverage FutureOr<T> for flexible async/sync returns.
  • Offload CPU-intensive tasks to isolates (e.g., image processing).
  • Use compute() for simple parallelism or Isolate.spawn for complex cases.
  • Use collection if/for/spreads for concise code.
  • Prefer .map()``.where()And .fold() over manual loops where appropriate.
  • Catch specific exceptions (on SomeException), not just all catch (e).
  • Use rethrow when needed to preserve stack traces.
  • Prefer Provider, Riverpod, Bloc, or GetX over setState
  • Avoid global state unless necessary (e.g., use scoped providers).
  • Follow BLoC/Cubit or MVVM patterns.
  • Keep UI (Widgets) and business logic (Models/Controllers) separate.
  • Reactive Programming:
  • Use StreamBuilder/FutureBuilder for async UI updates.
  • Avoid nested reactive widgets (e.g., minimize StreamBuilder layers).
  • State Persistence:
  • Use hydrated_bloc or shared_preferences for local state persistence.
  • Avoid prop drilling with Provider or Riverpod.
  • Use freezed for immutable models and unions.
  • Serialize JSON with json_serializable.
  • Use MethodChannel for native integrations (Kotlin/Swift).
  • Organize layers into data``domainAnd presentation.
  • Mark widgets as const when possible to prevent unnecessary rebuilds.
  • Use const constructors for children in lists/grids.
  • Avoid Opacity for animations; prefer AnimatedOpacity or Transform.
  • Use ListView.builder (or CustomScrollView) for infinite/large lists (lazy loading).
  • Extract expensive computations from build() methods.
  • Use KeyS (e.g., ValueKey``UniqueKey) when modifying collections of stateful widgets.
  • Use precacheImage for images loaded on-demand.
  • Use LayoutBuilder``MediaQueryOr SafeArea.
  • Test on multiple screen sizes (e.g., using DevicePreview).
  • Add semantic labels (Semantics widget), use ExcludeSemantics where needed.
  • Support dynamic text sizing (TextScaler).
  • Use intl package with ARB files or flutter_localizations.
  • Define a consistent ThemeData in MaterialApp.
  • Use ThemeExtensions for custom theming (Flutter 3+).
  • Use named routes with go_router for deep linking and simplified navigation.
  • Test performance in profile mode (flutter run --profile).
  • Use DevTools to check for jank, memory leaks, and CPU usage.
  • Dispose controllers (ScrollController``TextEditingController) and subscriptions.
  • Use const widgets to reduce garbage collection.
  • Minimize ClipPath``OpacityAnd ShaderMask usages in animations.
  • Prefer CustomPaint for complex UIs over deep widget trees.
  • Compress images (use .webp format).
  • Cache images with cached_network_image.
  • Unit Test with mockito or mocktail for mocking.
  • Use WidgetTester to verify UI behavior (e.g., pumpAndSettle).
  • Run end-to-end tests with integration_test.
  • Use golden_toolkit for pixel-perfect UI comparisons.
  • Generate reports with flutter test --coverage + lcov.
  • Use flutter analyze and dart fix.
  • Configure analysis_options.yaml with strict lints (flutter_lints package).
  • Pin versions in pubspec.yaml (^ for SemVer-compatible updates).
  • Audit dependencies with dart pub outdated.
  • Use doc comments (///) and generate docs via dart doc.
  • Keep README.md updated with project setup and architecture.
  • Use sentry_flutter for error tracking.
  • ndk version mismatch
  • Set ndkVersion in build.gradle.kts to the specific ndk version installed

Writing clean code: Best practices are like good manners — they make your code easier for others (and future you) to read, understand, and maintain.

Why it matters: Clean code is easier to debug, test, and extend. It reduces bugs and improves team productivity.

The key insight: Code is read much more often than it’s written — optimize for readability, not cleverness.

  1. Neglecting to normalise database designs, leading to data redundancy and update anomalies.

  2. Misunderstanding the difference between a stack (LIFO) and a queue (FIFO) in data structure applications.

  3. Writing pseudocode that is too language-specific rather than using standard algorithmic constructs.

  4. Forgetting that O(nlogn)O(n \log n) average-case for quicksort becomes O(n2)O(n^2) worst-case on already sorted input.

The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.