← All posts

10 Rules for Building Reliable Flutter Apps

In today's world, many developers are riding the AI wave and "vibe coding" trends — but let's not forget the time-tested rules that make our code resilient. Inspired by NASA/JPL's safety-critical coding guidelines from C, adapted for modern Flutter development.

Adapted from "The Power of 10: Rules for Developing Safety-Critical Code" by Gerard J. Holzmann

In safety-critical systems, every line of code must be written with an emphasis on predictability and clarity. Although Flutter apps typically aren't controlling life-and-death systems, the same principles of simplicity, verifiability, and maintainability can elevate your mobile applications. Below, we present ten adapted rules for Flutter development, with detailed explanations and practical Flutter examples.


1. Keep Widget Trees Simple

Original Rule: Restrict all code to very simple control flow constructs

In safety-critical C code, avoiding complex constructs like recursion and "goto" statements leads to easier analysis and fewer hidden bugs. In Flutter, simplicity in your widget tree is just as crucial.

  • What It Means in Flutter:

    • Avoid Deep Nesting: Break down complex screens into small, reusable widgets.
    • Enhance Readability: A flat, clear widget structure simplifies debugging and testing.
  • Example: Instead of writing one gigantic build method, extract parts of your UI:

    class ProfileScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            ProfileHeader(),
            ProfileDetails(),
            ProfileActions(),
          ],
        );
      }
    }
    
    class ProfileHeader extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Text('John Doe', style: Theme.of(context).textTheme.headline6);
      }
    }
    

2. Bound Your Asynchronous Operations and Loops

Original Rule: Give all loops a fixed upper bound

In safety-critical environments, ensuring loops terminate within known bounds is key to avoiding runaway code.

  • What It Means in Flutter:

    • Asynchronous Safety: When using features like FutureBuilder or polling, clearly define termination conditions or timeouts.
    • Prevent Unresponsive UIs: Avoid infinite asynchronous loops without proper exit conditions.
  • Example: Using a timeout on a network call to ensure the UI doesn't hang:

    Future<Data> fetchData() {
      return http.get(apiUrl).timeout(Duration(seconds: 10));
    }
    

3. Manage Memory Wisely

Original Rule: Do not use dynamic memory allocation after initialization

While Dart's garbage collector manages memory automatically, unbounded growth in collections or misuse of state can lead to performance degradation.

  • What It Means in Flutter:
    • Use Immutable Structures: Favor immutable widgets and const constructors to reduce unnecessary allocations.
    • Dispose Controllers: Always dispose controllers (e.g., TextEditingController, AnimationController) to free memory.
  • Example:
    class MyWidget extends StatefulWidget {
      @override
      _MyWidgetState createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends State<MyWidget> {
      final TextEditingController _controller = TextEditingController();
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return TextField(controller: _controller);
      }
    }
    

4. Keep Functions and Build Methods Concise

Original Rule: No function should be longer than what can be printed on one page

Long, sprawling functions are hard to understand and maintain.

  • What It Means in Flutter:
    • Extract Widgets: Keep your build methods lean by extracting portions of the UI into separate widget classes or methods.
    • Single Responsibility: Each function or widget should handle one logical unit of work.
  • Example: Instead of a 100-line build method, split your UI into smaller widgets as shown in Rule 1.

5. Use Assertions and Error Handling Generously

Original Rule: The code's assertion density should average two assertions per function

Assertions help catch errors early and document expected conditions.

  • What It Means in Flutter:
    • Assert in Development: Use Dart's assert statements in your code to enforce invariants.
    • Graceful Failures: Combine assertions with try/catch to handle unexpected states in production.
  • Example:
    Widget build(BuildContext context) {
      assert(items != null, 'Items list should not be null');
      return ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) => Text(items[index]),
      );
    }
    

6. Declare Variables in the Smallest Scope Possible

Original Rule: Declare all data objects at the smallest possible level of scope

Reducing variable scope minimizes unintended side effects and makes debugging easier.

  • What It Means in Flutter:
    • Localize State: Keep state and variables as local as possible within functions or widget classes.
    • Encapsulate Data: Use private variables (with an underscore) to restrict access.
  • Example:
    void someFunction() {
      final int count = 0; // Declared locally within the function.
      // Use 'count' only where needed.
    }
    

7. Always Handle Errors and Return Values

Original Rule: Check the return value of every nonvoid function and the validity of parameters

Ignoring errors can lead to silent failures.

  • What It Means in Flutter:
    • Error Handling in Async Code: Always use try/catch with asynchronous operations to catch network or parsing errors.
    • Validate Inputs: Use assertions and explicit checks for null or unexpected values.
  • Example:
    Future<void> loadUserData() async {
      try {
        final response = await http.get(userApi);
        if (response.statusCode != 200) {
          throw Exception('Failed to load user data');
        }
        // Process data...
      } catch (e) {
        // Log or handle error appropriately
        print('Error: $e');
      }
    }
    

8. Limit Conditional Code and Code Generation Complexity

Original Rule: Limit the use of the preprocessor to header inclusion and simple macros

While Dart doesn't have a preprocessor, similar caution applies to code generation and conditional imports.

  • What It Means in Flutter:

    • Simplicity Over Cleverness: Avoid overly complex code generation or conditional logic that obscures the code's intent.
    • Consistent Patterns: Stick to simple, standard solutions for internationalization, theming, or platform-specific code.
  • Example: Use straightforward conditional imports:

    import 'mobile_specific.dart'
        if (dart.library.html) 'web_specific.dart';
    

9. Favor Immutability and Controlled References

Original Rule: Restrict the use of pointers to a single level of dereferencing

In C, excessive pointer use can hide bugs; in Flutter, managing mutable state carefully is equally important.

  • What It Means in Flutter:
    • Immutable Widgets: Use immutable widgets and const constructors to prevent unexpected side effects.
    • State Management: Rely on robust state management solutions (like Provider, Riverpod, or BLoC) to manage mutable state in a controlled manner.
  • Example:
    class MyImmutableWidget extends StatelessWidget {
      const MyImmutableWidget({Key? key, required this.title}) : super(key: key);
      final String title;
    
      @override
      Widget build(BuildContext context) {
        return Text(title);
      }
    }
    

10. Compile and Analyze with Zero Warnings

Original Rule: All code must compile with all warnings enabled at the most pedantic setting

Warnings are early indicators of potential issues.

  • What It Means in Flutter:

    • Use Dart Analysis: Run dart analyze or use Flutter's built-in analysis tools regularly to catch and fix warnings.
    • Treat Warnings as Errors: Configure your CI/CD pipelines to fail builds on warnings, ensuring code quality is maintained over time.
  • Example: Add strict linting rules in your analysis_options.yaml:

    linter:
      rules:
        - avoid_print
        - always_declare_return_types
        - prefer_const_constructors
    

A Brief Note on the Origins in C and Safety-Critical Systems

The original "Power of 10" guidelines were developed for safety-critical C code at NASA's Jet Propulsion Laboratory (JPL) by Gerard J. Holzmann. In that context, strict rules were necessary to ensure that software controlling critical systems was verifiable and reliable. For example:

  • C Code Simplicity: In C, avoiding constructs like recursion or goto helped create a codebase that was easier to analyze with static tools, which was crucial when lives were at stake.
  • Manual Memory Management: C developers had to manage memory explicitly, making the discipline of minimal, controlled allocations vital for reliability.
  • Tool-Based Verification: The original guidelines emphasized using automated tools to enforce coding standards — a concept that translates into modern Flutter development via Dart's static analysis and Flutter's DevTools.

By adapting these rules to Flutter, we retain the core philosophy: writing code that is simple, testable, and maintainable. While Flutter's environment benefits from Dart's modern features (like garbage collection and strong type safety), the same principles help ensure that your UI remains robust and your state management is predictable.


Credits

The original "Power of 10: Rules for Developing Safety-Critical Code" by Gerard J. Holzmann (NASA/JPL) provided the inspiration for these adapted rules.


Conclusion

By embracing these ten rules in your Flutter development workflow, you create a codebase that is simple, reliable, and maintainable — qualities that are as crucial in safety-critical C systems as they are in modern mobile applications. Whether you're breaking down complex widget trees, managing asynchronous operations with clear bounds, or enforcing strict linting rules, these practices pave the way for robust, scalable, and high-performance Flutter apps.

Happy coding, and may your Flutter projects thrive!

← PREVWhy is Flutter Build Taking So Long? Tips to Speed Up Your Flutter Build Time