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:
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:
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:
@override
Future<Widget Function(BuildContext)> setup() async {
final data = await fetchData();
return (context) => Text(data);
}✅ Good:
@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:
use*helper functions (recommended)- 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:
@override
Widget Function(BuildContext) setup() {
final controller = ScrollController(); // Never disposed!
return (context) => ListView(controller: controller);
}✅ Good (Option 1 - Recommended):
@override
Widget Function(BuildContext) setup() {
final controller = useScrollController(); // Auto-disposed
return (context) => ListView(controller: controller.value);
}✅ Good (Option 2 - Manual):
@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:
useScrollController,usePageController,useFocusNode,useTextEditingController,useValueNotifier,useAnimationController,manageListenable,manageValueListenable
Examples
❌ Bad:
@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:
@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:
@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:
@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:
ref.value['key'] = newValue; // ❌
ref.value.property = newValue; // ❌
ref.value = {...ref.value, 'key': newValue}; // ✅Array element assignment:
ref.value[index] = newValue; // ❌
ref.value = [...ref.value.sublist(0, index), newValue, ...ref.value.sublist(index + 1)]; // ✅Mutating methods:
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 = []; // ✅Disabling Rules
Per-file
// ignore_for_file: flutter_compositions_ensure_reactive_propsPer-line
// ignore: flutter_compositions_ensure_reactive_props
final name = this.name;In analysis_options.yaml
custom_lint:
rules:
- flutter_compositions_ensure_reactive_props: false
- flutter_compositions_no_async_setup: trueContributing
Have suggestions for new rules or improvements to existing ones? Please open an issue or pull request!