Skip to content

Flutter Compositions Lint Rules ​

Complete reference for all available lint rules.

Rule Categories ​

  • Reactivity: Rules ensuring proper reactive state management
  • Lifecycle: Rules managing component lifecycle and resource cleanup
  • Best Practices: General best practice rules

Reactivity Rules ​

flutter_compositions_ensure_reactive_props ​

Category: Reactivity Severity: Warning Auto-fixable: No

Description ​

Ensures that widget properties are accessed through widget() in the setup() method to maintain reactivity. Direct property access will not trigger reactive updates.

Why it matters ​

The setup() method runs only once. If you directly access this.propertyName, you capture a snapshot of the value at setup time. When the parent passes new props, your component won't react to the change.

Examples ​

❌ Bad:

dart
class UserCard extends CompositionWidget {
  final String name;

  @override
  Widget Function(BuildContext) setup() {
    // Captures initial value only - NOT reactive
    final greeting = 'Hello, $name!';
    return (context) => Text(greeting);
  }
}

✅ Good:

dart
class UserCard extends CompositionWidget {
  final String name;

  @override
  Widget Function(BuildContext) setup() {
    final props = widget();
    // Reacts to prop changes
    final greeting = computed(() => 'Hello, ${props.value.name}!');
    return (context) => Text(greeting.value);
  }
}

Lifecycle Rules ​

flutter_compositions_no_async_setup ​

Category: Lifecycle Severity: Error Auto-fixable: No

Description ​

Prevents setup() methods from being async. The setup function must synchronously return a builder function.

Why it matters ​

Making setup() async breaks the composition lifecycle. The framework expects a synchronous builder function return, and async setups can cause timing issues and unpredictable behavior.

Examples ​

❌ Bad:

dart
@override
Future<Widget Function(BuildContext)> setup() async {
  final data = await fetchData();
  return (context) => Text(data);
}

✅ Good:

dart
@override
Widget Function(BuildContext) setup() {
  final data = ref<String?>(null);

  onMounted(() async {
    data.value = await fetchData();
  });

  return (context) => Text(data.value ?? 'Loading...');
}

flutter_compositions_controller_lifecycle ​

Category: Lifecycle Severity: Warning Auto-fixable: No

Description ​

Ensures Flutter controllers (ScrollController, TextEditingController, etc.) are properly disposed using either:

  1. use* helper functions (recommended)
  2. Manual disposal in onUnmounted()

Why it matters ​

Controllers hold native resources and listeners. Failing to dispose them causes memory leaks.

Detected controller types ​

  • ScrollController
  • PageController
  • TextEditingController
  • TabController
  • AnimationController
  • VideoPlayerController
  • WebViewController

Examples ​

❌ Bad:

dart
@override
Widget Function(BuildContext) setup() {
  final controller = ScrollController(); // Never disposed!
  return (context) => ListView(controller: controller);
}

✅ Good (Option 1 - Recommended):

dart
@override
Widget Function(BuildContext) setup() {
  final controller = useScrollController(); // Auto-disposed
  return (context) => ListView(controller: controller.raw); // .raw avoids unnecessary rebuilds
}

✅ Good (Option 2 - Manual):

dart
@override
Widget Function(BuildContext) setup() {
  final controller = ScrollController();
  onUnmounted(() => controller.dispose());
  return (context) => ListView(controller: controller);
}

flutter_compositions_no_conditional_composition ​

Category: Lifecycle Severity: Error Auto-fixable: No

Description ​

Ensures composition API calls (ref(), computed(), watch(), useScrollController(), etc.) are not placed inside conditionals or loops. Similar to React Hooks rules, composition APIs must be called unconditionally at the top level of setup().

Why it matters ​

Conditional composition API calls can cause:

  • Inconsistent ordering of reactive dependencies across renders
  • Unpredictable reactivity behavior
  • Difficult to debug lifecycle issues
  • Memory leaks when cleanup hooks are skipped

Flagged composition APIs ​

  • Reactivity: ref, computed, writableComputed, customRef, watch, watchEffect
  • Lifecycle: onMounted, onUnmounted
  • Dependency injection: provide, inject
  • Controllers: useController, useScrollController, usePageController, useFocusNode, useTextEditingController, useAnimationController, manageListenable, manageValueListenable

Examples ​

❌ Bad:

dart
@override
Widget Function(BuildContext) setup() {
  if (someCondition) {
    final count = ref(0); // ❌ Conditional composition API
  }

  for (var i = 0; i < 10; i++) {
    final item = ref(i); // ❌ Inside loop
  }

  return (context) => Text('Hello');
}

✅ Good:

dart
@override
Widget Function(BuildContext) setup() {
  // ✅ Composition APIs at top level
  final count = ref(0);
  final items = ref(<int>[]);

  // ✅ Conditional logic for values is OK
  if (someCondition) {
    count.value = 10;
  }

  return (context) => Text('Count: ${count.value}');
}

Best Practices Rules ​

flutter_compositions_shallow_reactivity ​

Category: Best Practices Severity: Warning Auto-fixable: No

Description ​

Warns about shallow reactivity limitations. Flutter Compositions uses shallow reactivity - only reassigning .value triggers updates. Directly mutating properties or array elements will NOT trigger reactive updates.

Why it matters ​

The reactivity system tracks changes to ref.value itself, not changes within the object or array. Direct mutations like ref.value.property = x or ref.value[0] = x won't notify subscribers, leading to stale UI.

Examples ​

❌ Bad:

dart
@override
Widget Function(BuildContext) setup() {
  final user = ref({'name': 'John', 'age': 30});
  final items = ref([1, 2, 3]);

  void updateUser() {
    user.value['name'] = 'Jane'; // Won't trigger update!
  }

  void updateItems() {
    items.value[0] = 10; // Won't trigger update!
    items.value.add(4); // Won't trigger update!
  }

  return (context) => Column(
    children: [
      Text(user.value['name']),
      Text('${items.value[0]}'),
    ],
  );
}

✅ Good:

dart
@override
Widget Function(BuildContext) setup() {
  final user = ref({'name': 'John', 'age': 30});
  final items = ref([1, 2, 3]);

  void updateUser() {
    // Create new object to trigger update
    user.value = {...user.value, 'name': 'Jane'};
  }

  void updateItems() {
    // Create new array to trigger update
    items.value = [10, ...items.value.sublist(1)];
    items.value = [...items.value, 4];
  }

  return (context) => Column(
    children: [
      Text(user.value['name']),
      Text('${items.value[0]}'),
    ],
  );
}

Common mutation patterns to avoid ​

Direct property assignment:

dart
ref.value['key'] = newValue; // ❌
ref.value.property = newValue; // ❌
ref.value = {...ref.value, 'key': newValue}; // ✅

Array element assignment:

dart
ref.value[index] = newValue; // ❌
ref.value = [...ref.value.sublist(0, index), newValue, ...ref.value.sublist(index + 1)]; // ✅

Mutating methods:

dart
ref.value.add(item); // ❌
ref.value.remove(item); // ❌
ref.value.clear(); // ❌
ref.value = [...ref.value, item]; // ✅
ref.value = ref.value.where((x) => x != item).toList(); // ✅
ref.value = []; // ✅

flutter_compositions_no_logic_in_builder ​

Category: Best Practices Severity: Warning Auto-fixable: No

Description ​

Prevents logic inside the builder function returned by setup(). The builder should only build the widget tree — all computations, conditionals, and side effects belong in setup() using computed, watch, or composables. The only exception is props destructuring (e.g., final MyWidget(:title) = props.value;).

Why it matters ​

Logic in the builder re-executes on every rebuild, which defeats the purpose of the composition model. By moving logic into setup() with computed, values are cached and only recalculated when their dependencies change.

Examples ​

❌ Bad:

dart
@override
Widget Function(BuildContext) setup() {
  final items = ref(<Item>[]);
  final filter = ref('');

  return (context) {
    // ❌ Filtering logic should be a computed in setup()
    final filtered = items.value
        .where((item) => item.name.contains(filter.value))
        .toList();
    return ListView(children: filtered.map(ItemTile.new).toList());
  };
}

✅ Good:

dart
@override
Widget Function(BuildContext) setup() {
  final items = ref(<Item>[]);
  final filter = ref('');
  final filtered = computed(
    () => items.value.where((item) => item.name.contains(filter.value)).toList(),
  );

  return (context) => ListView(
    children: filtered.value.map(ItemTile.new).toList(),
  );
}

flutter_compositions_prefer_raw_controller ​

Category: Best Practices Severity: Warning Auto-fixable: No

Description ​

Suggests using .raw instead of .value when passing controller refs to widget parameters like controller:, focusNode:, scrollController:, or animationController:. Reading .value subscribes the builder to changes in the ref, but controllers rarely change after creation — .raw reads without tracking to avoid unnecessary rebuilds.

Why it matters ​

Controllers returned by use* helpers (e.g., useScrollController()) are wrapped in a Ref. Using .value inside the builder creates a reactive subscription, meaning the builder re-runs whenever the signal fires. Since the controller object itself doesn't change, .raw provides the same value without the overhead.

Examples ​

❌ Bad:

dart
@override
Widget Function(BuildContext) setup() {
  final scrollController = useScrollController();

  return (context) => ListView(
    controller: scrollController.value, // ❌ Subscribes unnecessarily
  );
}

✅ Good:

dart
@override
Widget Function(BuildContext) setup() {
  final scrollController = useScrollController();

  return (context) => ListView(
    controller: scrollController.raw, // ✅ Reads without tracking
  );
}

Disabling Rules ​

Per-file ​

dart
// ignore_for_file: flutter_compositions_ensure_reactive_props

Per-line ​

dart
// ignore: flutter_compositions_ensure_reactive_props
final name = this.name;

In analysis_options.yaml ​

yaml
plugins:
  flutter_compositions_lints:
    path: .

Contributing ​

Have suggestions for new rules or improvements to existing ones? Please open an issue or pull request!

Released under the MIT License.