Kaeru is a comprehensive and efficient reactivity system for Flutter, inspired by Vue 3's @vue/reactivity
. It provides a fully functional reactive programming model that makes state management in Flutter simple, optimized, and declarative.
- Fully reactive state management with
Ref
,Computed
,AsyncComputed
, andwatchEffect
. - Automatic dependency tracking for efficient updates.
- Supports both synchronous and asynchronous computed values.
- Optimized UI updates with
Watch
andKaeruMixin
. - Seamless integration with ChangeNotifier and ValueNotifier.
Tip
If in KaeruMixin you have use watch$
with watch
and watchEffect$
with watchEffect
useContext
useHoverWidget
useRef
useState
useWidgetBounding
useWidgetSize
Add this package to your pubspec.yaml
:
dependencies:
kaeru:
git:
url: https://github.com/tachibana-shin/flutter_kaeru.git
Import it in your project:
import 'package:kaeru/kaeru.dart';
View example. All life
and reactivity
ready with name $ + <name>
(e.g: $ref
, $computed
, $watch
, $onBeforeUnmount
...)
typedef FooProps = ({Ref<int> counter});
// ignore: non_constant_identifier_names
final Foo = defineWidget((FooProps props) {
final foo = $ref(0);
void onPressed() {
foo.value++;
}
void resetParent() {
props.counter.value = 0;
}
return (ctx) => Row(
children: [
TextButton(
onPressed: onPressed,
child:
Text('counter + foo = ${props.counter.value + foo.value}')),
TextButton(
onPressed: resetParent, child: Text('Reset counter parent'))
],
);
});
typedef CounterProps = ({VoidCallback onMax});
// ignore: non_constant_identifier_names
final Counter = defineWidget((CounterProps props) {
final counter = $ref(0);
print('Render once');
void onPressed() {
counter.value++;
if (counter.value > 10) props.onMax();
}
return (ctx) => Row(
children: [
TextButton(
onPressed: onPressed, child: Text('counter = $counter.value')),
Foo((counter: counter))
],
);
});
Represents a reactive variable that automatically triggers updates when changed.
Parameter | Type | Description |
---|---|---|
value |
T |
The initial value of the reactive reference. |
Method | Returns | Description |
---|---|---|
select<U>(U Function(T value)) |
Computed<U> |
Creates a computed value derived from this Ref<T> . |
final count = Ref(0);
count.addListener(() {
print("Count changed: ${count.value}");
});
count.value++; // β
Triggers update
final doubleCount = count.select((v) => v * 2);
print(doubleCount.value); // β
0
count.value = 5;
print(doubleCount.value); // β
10
Creates a computed value that automatically updates when dependencies change.
Parameter | Type | Description |
---|---|---|
getter |
T Function() |
A function that returns the computed value. |
Method | Returns | Description |
---|---|---|
select<U>(U Function(T value)) |
Computed<U> |
Creates a derived computed value. |
final count = Ref(2);
final doubleCount = Computed(() => count.value * 2);
print(doubleCount.value); // β
4
count.value++;
print(doubleCount.value); // β
6
final tripleCount = doubleCount.select((v) => v * 1.5);
print(tripleCount.value); // β
9
- Automatically tracks dependencies and re-executes when values change.
final stop = watchEffect$(() {
print("Count is now: ${count.value}");
});
count.value++; // β
Automatically tracks dependencies
stop(); // β
Stops watching
- Watches multiple
ChangeNotifier
sources. - If
immediate = true
, executes the callback immediately.
final stop = watch$([count], () {
print("Count changed: ${count.value}");
}, immediate: true);
stop(); // β
Stops watching
Handles computed values that depend on asynchronous operations.
Parameter | Type | Description |
---|---|---|
getter |
Future<T> Function() |
A function returning a future value. |
defaultValue |
T? |
An optional initial value before computation completes. |
beforeUpdate |
T? Function() |
An optional function to run before updating the value. |
notifyBeforeUpdate |
bool = true |
Whether to notify listeners before updating the value. |
onError |
Function(dynamic error)? |
An optional error handler. |
immediate |
bool |
Whether to compute immediately. |
final asyncData = AsyncComputed(() async {
await Future.delayed(Duration(seconds: 1));
return "Loaded";
}, defaultValue: "Loading", onError: (e) => print("Error: $e"), immediate: true);
print(asyncData.value); // β
"Loading"
await Future.delayed(Duration(seconds: 1));
print(asyncData.value); // β
"Loaded"
Allows stateful widgets to easily integrate with reactive values.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with KaeruMixin {
late final Ref<int> count;
@override
void initState() {
super.initState();
count = ref(0);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Watch(() => Text("Count: ${count.value}")),
ElevatedButton(
onPressed: () => count.value++,
child: Text("Increment"),
),
],
);
}
}
A widget that automatically updates when its dependencies change.
[!TIP] If in KaeruMixin you have use
watch$
withwatch
andwatchEffect$
withwatchEffect
By default Watch
doesn"t care about external changes e.g.
class ExampleState extends State<Example> {
int counter = 1;
@override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
counter++;
});
});
}
@override
Widget build(context) {
return Watch(() => Text('counter = $counter')); // every is 'counter = 1'
}
}
so if static dependency is used in Watch
you need to set it in the dependencies
option
class ExampleState extends State<Example> {
int counter = 1;
@override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
counter++;
});
});
}
@override
Widget build(context) {
return Watch(dependencies: [counter], () => Text('counter = $counter')); // amazing, now 'counter = 1', 'counter = 2'....
}
}
Watch(
() => Text("Value: ${count.value}"),
)
KaeruBuilder((context) {
final counter = context.ref(0);
return Watch(() => Text(counter.value.toString()));
});
Converts a ValueNotifier<T>
into a Ref<T>
.
final valueNotifier = ValueNotifier(0);
final ref = valueNotifier.toRef();
ref.addListener(() {
print("Updated: ${ref.value}");
});
valueNotifier.value = 10; // β
Ref updates automatically
Adds .toRef()
to ValueNotifier
to integrate seamlessly.
Creates a computed value that tracks only the selected part of a reactive object, optimizing updates.
Parameter | Type | Description |
---|---|---|
ctx |
ReactiveNotifier<T> |
The reactive object to select from. |
selector |
U Function(T value) |
Function to select a value from the object. |
final map = Ref({'foo': 0, 'bar': 0});
Watch(() {
// Only recomputes when 'foo' changes
final foo = usePick(() => map.value['foo']);
print(foo.value); // 0
});
map.value = {...map.value, 'foo': 1};
Registers a callback to be called when the watcher or computed is refreshed or disposed.
Parameter | Type | Description |
---|---|---|
callback |
VoidCallback |
Function to be called on cleanup/dispose. |
watchEffect$(() {
// ... reactive code ...
onWatcherCleanup(() {
// cleanup logic here
});
});
// or widget Watch
Watch(() {
onWatcherCleanup(() {
// cleanup logic here
});
////
});
// or Computed
Computed(() {
onWatcherCleanup(() {
// cleanup logic here
});
////
});
Runs a callback after the current microtask queue is flushed (similar to Promise.resolve().then()
in JS).
Parameter | Type | Description |
---|---|---|
callback |
VoidCallback? |
(Optional) Function to run after the tick. |
await nextTick();
// or
await nextTick(() {
// code to run after the tick
});
KaeruLifeMixin and KaeruListenMixin are powerful mixins designed to simplify Flutter development by providing Vue-like lifecycle hooks and reactive state listening.
β Cleaner code: No need to override multiple lifecycle methods or manage listeners manually. β Reusable: Apply them to any StatefulWidget to enhance reactivity. β Inspired by Vue: Provides a familiar development experience for reactive state management.
KaeruLifeMixin provides Vue-like lifecycle hooks for StatefulWidget
. It enables multiple callbacks for different lifecycle events.
onMounted()
: Called when the widget is first created (initState
).onDependenciesChanged()
: Called when dependencies change (didChangeDependencies
).onUpdated()
: Called when the widget receives updated properties (didUpdateWidget
).onDeactivated()
: Called when the widget is temporarily removed (deactivate
).onBeforeUnmount()
: Called just before the widget is disposed (dispose
).
class MyComponent extends StatefulWidget {
@override
_MyComponentState createState() => _MyComponentState();
}
class _MyComponentState extends State<MyComponent> with KaeruLifeMixin<MyComponent> {
@override
void initState() {
super.initState();
onMounted(() => print('β
Widget Mounted!'));
onDependenciesChanged(() => print('π Dependencies Changed!'));
onUpdated(() => print('β»οΈ Widget Updated!'));
onDeactivated(() => print('β οΈ Widget Deactivated!'));
onBeforeUnmount(() => print('π Widget Disposed!'));
}
@override
Widget build(BuildContext context) {
return Text('KaeruLifeMixin Example');
}
}
KaeruListenMixin simplifies listening to ChangeNotifier
updates within a StatefulWidget
. It allows adding listeners dynamically and managing their cleanup automatically.
listen()
: Subscribes to a singleChangeNotifier
and executes a callback when it changes.listenAll()
: Subscribes to multipleChangeNotifiers
with a single callback.- Returns a cancel function to remove listeners when necessary.
class MyNotifier extends ChangeNotifier {
void update() {
notifyListeners();
}
}
class MyComponent extends StatefulWidget {
@override
_MyComponentState createState() => _MyComponentState();
}
class _MyComponentState extends State<MyComponent> with KaeruListenMixin<MyComponent> {
final myNotifier = MyNotifier();
VoidCallback? cancelListener;
@override
void initState() {
super.initState();
cancelListener = listen(myNotifier, () {
print('Single notifier changed!');
});
}
@override
void dispose() {
cancelListener?.call();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Listening to a single ChangeNotifier');
}
}
class NotifierA extends ChangeNotifier {
void update() => notifyListeners();
}
class NotifierB extends ChangeNotifier {
void update() => notifyListeners();
}
class MyComponent extends StatefulWidget {
@override
_MyComponentState createState() => _MyComponentState();
}
class _MyComponentState extends State<MyComponent> with KaeruListenMixin<MyComponent> {
final notifierA = NotifierA();
final notifierB = NotifierB();
VoidCallback? cancelListeners;
@override
void initState() {
super.initState();
cancelListeners = listenAll([notifierA, notifierB], () {
print('One of the notifiers changed!');
});
}
@override
void dispose() {
cancelListeners?.call();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Listening to multiple ChangeNotifiers');
}
}
Feature | KaeruLifeMixin | KaeruListenMixin |
---|---|---|
Lifecycle Hooks | β
Provides onMounted , onUpdated , onBeforeUnmount , etc. |
β Not applicable |
Reactive Listeners | β Not applicable | β
Handles ChangeNotifier updates |
Automatic Cleanup | β Hooks are executed at proper lifecycle stages | β Listeners are removed automatically |
Code Simplicity | β Reduces the need for overriding multiple lifecycle methods | β
Manages ChangeNotifier subscriptions easily |
π KaeruLifeMixin is perfect for handling widget lifecycle events.
π KaeruListenMixin makes managing ChangeNotifier
listeners easy.
Feature | Supported |
---|---|
Ref<T> |
β |
Computed<T> |
β |
AsyncComputed<T> |
β |
watchEffect |
β |
watch |
β |
KaeruMixin |
β |
Watch Widget |
β |
ValueNotifier.toRef() |
β |
ReactiveNotifier<T> |
β |
This package provides an intuitive and efficient reactivity system for Flutter, making state management much easier and more performant. π
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:kaeru/kaeru.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("Kaeru Example")),
body: Padding(padding: const EdgeInsets.all(16.0), child: App2())));
}
}
class App2 extends StatelessWidget {
final showCounter = Ref(true);
App2({super.key});
@override
Widget build(context) {
return Watch(() => Row(children: [
ElevatedButton(
onPressed: () {
showCounter.value = !showCounter.value;
},
child: Text(showCounter.value ? 'hide' : 'show'),
),
if (showCounter.value) Counter()
]));
}
}
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> with KaeruMixin, KaeruLifeMixin {
late final foo = ref<int>(0);
late final fooDouble = computed(() => foo.value * 2);
late final fooDoublePlus = Computed<int>(() => foo.value + 1);
late final fooGtTeen = computed<bool>(() {
print('Computed call');
return fooDouble.value > 10;
});
late final computedOnlyListen = computed(() => foo.value);
final bar = Ref<int>(0);
@override
void initState() {
watchEffect(() {
print('watchEffect run');
if (fooDoublePlus.value % 2 == 0) return;
print('foo + bar = ${foo.value + bar.value}');
});
watch$([computedOnlyListen], () {
print('computed only listen changed');
});
onMounted(() => print('β
Widget Mounted!'));
onDependenciesChanged(() => print('π Dependencies Changed!'));
onUpdated(() => print('β»οΈ Widget Updated!'));
onDeactivated(() => print('β οΈ Widget Deactivated!'));
onBeforeUnmount(() => print('π Widget Disposed!'));
super.initState();
}
@override
Widget build(BuildContext context) {
print('Root render');
return Column(
children: [
ElevatedButton(
onPressed: () {
foo.value++;
},
child: const Text("Increase foo"),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
bar.value++;
},
child: const Text("Increase bar"),
),
const SizedBox(height: 16),
Watch(() {
print('Watch render');
if (fooGtTeen.value) {
return Watch(() {
print('Watch child 1 render');
return Text("Bar: ${bar.value}");
});
} else {
return Text("Bar: ${bar.value}");
}
}),
Watch(() {
print('Widget parent ShowFoo render');
return bar.value % 2 == 0 ? SizedBox.shrink() : ShowFoo(foo: foo);
})
],
);
}
}
class ShowFoo extends StatefulWidget {
final Ref<int> foo;
const ShowFoo({super.key, required this.foo});
@override
createState() => _ShowFooState();
}
class _ShowFooState extends State<ShowFoo> with KaeruListenMixin, KaeruMixin {
late final _fooDouble = computed(() {
print('ShowFoo computed emit change');
return widget.foo.value * 2;
});
@override
void initState() {
listen(widget.foo, () {
print('ShowFoo emit change foo ${widget.foo.value}');
});
super.initState();
}
@override
Widget build(context) {
return Column(children: [
Watch(() => Text('ShowFoo: ${widget.foo.value}')),
Watch(() => Text('foo * 2 = ${_fooDouble.value}'))
]);
}
}
Pull requests and feature requests are welcome! Feel free to open an issue or contribute.
MIT License. See LICENSE for details.