flutter_compositions 0.1.1
flutter_compositions: ^0.1.1 copied to clipboard
Reactive composition primitives for Flutter inspired by the Vue 3 Composition API.
Flutter Compositions #
Vue-inspired reactive building blocks for Flutter
Flutter Compositions brings Vue 3's Composition API patterns to Flutter, enabling fine-grained reactivity and composable logic with a clean, declarative API.
Documentation #
📚 Read the full documentation →
- Getting Started - Quick start guide and installation
- Guide - Learn core concepts and patterns
- API Reference - Complete API documentation
- Internals - Architecture and design decisions
Features #
- ✨ Vue-inspired API - Familiar
ref,computed,watch, andwatchEffectfor reactive state - 🎯 Fine-grained reactivity - Powered by
alien_signalsfor minimal rebuilds - 🔧 Composable logic - Extract and reuse stateful logic with custom composables
- 💉 Type-safe DI -
provide/injectwithInjectionKeyfor zero conflicts - 🎨 Flutter integration - Built-in composables for controllers, animations, async data, and more
- 📦 Zero boilerplate - Single
setup()function replacesinitState,dispose, anddidUpdateWidget - 🛡️ Lint rules - Custom lints enforce reactivity best practices
Installation #
dependencies:
flutter_compositions: ^0.1.0
dev_dependencies:
flutter_compositions_lints: ^0.1.0
custom_lint: ^0.7.0
Quick Start #
import 'package:flutter/material.dart';
import 'package:flutter_compositions/flutter_compositions.dart';
class CounterPage extends CompositionWidget {
const CounterPage({super.key});
@override
Widget Function(BuildContext) setup() {
// Reactive state
final count = ref(0);
final doubled = computed(() => count.value * 2);
// Side effects
watch(() => count.value, (value, previous) {
debugPrint('count: $previous → $value');
});
// Lifecycle
onMounted(() => debugPrint('Mounted!'));
// Return builder
return (context) => Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Count: ${count.value}', style: Theme.of(context).textTheme.headlineMedium),
Text('Doubled: ${doubled.value}'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => count.value++,
child: const Icon(Icons.add),
),
);
}
}
Core Concepts #
Reactive State #
// Writable ref
final count = ref(0);
count.value++; // Updates trigger rebuilds
// Computed (derived state)
final doubled = computed(() => count.value * 2);
// Writable computed
final userName = writableComputed(
getter: (get) => get(firstName) + ' ' + get(lastName),
setter: (value, set) {
final parts = value.split(' ');
set(firstName, parts[0]);
set(lastName, parts[1]);
},
);
Side Effects #
// Watch specific values
watch(
() => count.value,
(newValue, oldValue) {
print('Changed: $oldValue → $newValue');
},
);
// Watch effect (auto-tracks dependencies)
watchEffect(() {
print('Count or doubled changed: ${count.value}, ${doubled.value}');
});
Reactive Props #
class UserCard extends CompositionWidget {
const UserCard({super.key, required this.userId, required this.name});
final String userId;
final String name;
@override
Widget Function(BuildContext) setup() {
final props = widget(); // Reactive access to widget instance
// Reacts to prop changes
final greeting = computed(() => 'Hello, ${props.value.name}!');
watch(() => props.value.userId, (newId, oldId) {
debugPrint('User changed: $oldId → $newId');
});
return (context) => Text(greeting.value);
}
}
Dependency Injection #
// Define keys (usually as global constants)
final themeKey = InjectionKey<Ref<AppTheme>>('theme');
// Provider
class App extends CompositionWidget {
@override
Widget Function(BuildContext) setup() {
final theme = ref(AppTheme.dark());
provide(themeKey, theme);
return (context) => MaterialApp(home: HomePage());
}
}
// Consumer
class ThemedButton extends CompositionWidget {
@override
Widget Function(BuildContext) setup() {
final theme = inject(themeKey);
return (context) => ElevatedButton(
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(theme.value.primary)),
child: Text('Button'),
);
}
}
Built-in Composables #
Controllers #
// ScrollController with auto-disposal
final scrollController = useScrollController();
// TextEditingController with reactive text
final (controller, text, selection) = useTextEditingController(text: 'Hello');
final upperText = computed(() => text.value.toUpperCase());
// Other controllers
final pageController = usePageController();
final focusNode = useFocusNode();
Animations #
// AnimationController with auto-disposal and reactive value
final (controller, animValue) = useAnimationController(
duration: Duration(seconds: 2),
);
onMounted(() => controller.repeat());
// Use in builder
return (context) => Transform.rotate(
angle: animValue.value * 2 * pi,
child: Icon(Icons.refresh),
);
Async Operations #
// Future with state tracking
final userData = useFuture(() => fetchUser(userId));
return (context) {
return switch (userData.value) {
AsyncLoading() => CircularProgressIndicator(),
AsyncError(:final errorValue) => Text('Error: $errorValue'),
AsyncData(:final value) => Text('User: ${value.name}'),
AsyncIdle() => SizedBox.shrink(),
};
};
// Async data with watch and refresh
final (status, refresh) = useAsyncData<User, int>(
(userId) => api.fetchUser(userId),
watch: () => userId.value, // Auto-refetch on userId change
);
// Stream tracking
final count = useStream(
Stream.periodic(Duration(seconds: 1), (i) => i),
initialValue: 0,
);
Framework Integration #
// App lifecycle
final lifecycleState = useAppLifecycleState();
watch(() => lifecycleState.value, (state, _) {
if (state == AppLifecycleState.paused) {
saveState();
}
});
// Search controller
final searchController = useSearchController();
final searchText = computed(() => searchController.value.text);
Custom Composables #
Extract reusable logic into composables:
// Define composable
(Ref<int>, void Function()) useCounter({int initialValue = 0}) {
final count = ref(initialValue);
void increment() => count.value++;
return (count, increment);
}
// Use in widgets
class CounterWidget extends CompositionWidget {
@override
Widget Function(BuildContext) setup() {
final (count, increment) = useCounter(initialValue: 10);
return (context) => ElevatedButton(
onPressed: increment,
child: Text('Count: ${count.value}'),
);
}
}
Lint Rules #
Enable custom lints to enforce best practices:
# analysis_options.yaml
analyzer:
plugins:
- custom_lint
custom_lint:
enable_all_lint_rules: true
Available rules:
flutter_compositions_ensure_reactive_props- Ensure reactive prop access viawidget()flutter_compositions_no_async_setup- Prevent async setup methodsflutter_compositions_controller_lifecycle- Ensure proper controller disposalflutter_compositions_no_mutable_fields- Enforce immutable widget fieldsflutter_compositions_provide_inject_type_match- Warn against common type conflicts
Examples #
Check out the example app for more patterns:
cd packages/flutter_compositions/example
flutter run
Contributing #
Contributions are welcome! Please read our contributing guidelines first.
Acknowledgments #
Flutter Compositions is built upon excellent work from the open source community:
- alien_signals - Provides the core reactivity system with fine-grained signal-based state management
- flutter_hooks - Inspired composable patterns and demonstrated the viability of composition APIs in Flutter
We are grateful to these projects and their maintainers for paving the way.
License #
MIT © 2025