Kaeru for Flutter
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.
🚀 Core Features
- 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
📦 Installation
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';
New API KaeruWidget
The setup()
method returns a Widget Function()
, which acts as your build
method.
class CounterWidget extends KaeruWidget<CounterWidget> {
final int depend;
const CounterWidget({super.key, required this.depend});
@override
Setup setup() {
final count = ref(0);
final depend = prop((w) => w.depend);
watchEffect(() {
count.value; // track
debugPrint('effect in count changed ${count.value}');
});
watchEffect(() {
depend.value; // track
debugPrint('effect in depend changed ${depend.value}');
});
onMounted(() {
debugPrint('mounted');
});
return () {
debugPrint('main counter re-build');
return Row(children: [
Watch(() {
debugPrint('depend in counter changed');
return Text('depend = ${depend.value}');
}),
Watch(() {
debugPrint('counter in counter changed');
return Text('counter = ${count.value}');
}),
IconButton(onPressed: () => count.value++, icon: const Icon(Icons.add)),
IconButton(
onPressed: () => count.value--, icon: const Icon(Icons.remove)),
]);
};
}
}
You can even create compositions with this
Computed<double> useScaleWidth(Ref<double> ref) {
final ctx = useKaeruContext();
assert(ctx != null); // Ensure if use composition without KaeruWidget
final screenWidth = ctx.size.width;
return computed(() => ref.value / screenWidth);
}
- All hooks life and reactivity ready
useContext() -> BuildContext
useWidget<T> -> T is Widget
Kaeru Hooks & Widgets (Full List)
1. Lifecycle / Core Hooks
Hook / Composable | Description | Notes |
---|---|---|
onBeforeUnmount |
Register a callback to run before widget unmounts | Auto dispose / cleanup |
onUpdated |
Register a callback when reactive value changes | Works with Computed or Ref |
useLifeContext |
Access current Kaeru lifecycle context | Internal reactive context |
useKaeruContext |
Access current Kaeru reactive context | Returns current KaeruMixin |
onMounted |
Register a callback after widget is mounted | Runs once |
onUnmounted |
Register a callback after widget is disposed | Runs once |
2. Reactive / Controller Hooks (New)
Hook / Composable | Description | Notes |
---|---|---|
useTabController |
Creates a TabController with automatic dispose |
Requires TickerProvider |
useAnimationController |
Creates an AnimationController with automatic dispose |
Needs vsync |
useScrollController |
Creates a ScrollController with automatic dispose |
For ListView / ScrollView |
useTextEditingController |
Creates a TextEditingController with automatic dispose |
Can bind reactively |
usePageController |
Creates a PageController with automatic dispose |
For PageView |
useValueNotifier<T> |
Creates a ValueNotifier<T> with automatic dispose |
Reactive Kaeru binding |
useFocusNode |
Creates a FocusNode with automatic dispose |
For focus management |
useTicker |
Creates a low-level Ticker with automatic dispose |
Custom animation without AnimationController |
useKeepAliveClient |
Creates a KeepAliveClient with automatic dispose |
Simply calling this hook will make your widget behave as if it were a widget with KeepAliveClientMixin. |
useRestoration |
Creates a RestorationController with automatic dispose |
For restoration management |
useSingleTickerState |
Creates a SingleTickerProviderStateMixin with automatic dispose |
For custom animation without AnimationController |
3. Asynchronous Data Fetching Hooks
Hook / Composable | Description |
---|---|
useRequest |
Manages asynchronous data fetching with support for loading, error, and data states. |
usePagination |
Simplifies paginated data fetching. |
useLoadMore |
Handles "load more" or infinite scrolling logic. |
usePolling |
Repeatedly calls a service at a specified interval. |
Usage Examples
useRequest
For basic data fetching.
class UserWidget extends KaeruWidget {
@override
Setup setup() {
final controller = useRequest(() => fetchUser('123'));
return () {
if (controller.loading.value) {
return CircularProgressIndicator();
}
if (controller.error.value != null) {
return Text('Error: ${controller.error.value}');
}
return Text('User: ${controller.data.value?.name}');
};
}
}
usePagination
For paginated data.
class UserList extends KaeruWidget {
@override
Setup setup() {
final controller = usePagination((page, pageSize) => fetchUsers(page, pageSize));
return () {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: controller.data.value.length,
itemBuilder: (context, index) {
final user = controller.data.value[index];
return ListTile(title: Text(user.name));
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.arrow_back),
onPressed: controller.page.value > 1 ? () => controller.changePage(controller.page.value - 1) : null,
),
Text('Page ${controller.page.value}'),
IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => controller.changePage(controller.page.value + 1),
),
],
),
],
);
};
}
}
useLoadMore
For infinite scrolling.
class PostList extends KaeruWidget {
@override
Setup setup() {
final controller = useLoadMore((page, lastItem) => fetchPosts(page));
return () {
return ListView.builder(
itemCount: controller.data.value.length + 1,
itemBuilder: (context, index) {
if (index == controller.data.value.length) {
return TextButton(
onPressed: controller.loadMore,
child: Text('Load More'),
);
}
final post = controller.data.value[index];
return ListTile(title: Text(post.title));
},
);
};
}
}
usePolling
For periodically fetching data.
class Notifications extends KaeruWidget {
@override
Setup setup() {
final controller = usePolling(() => fetchNotifications(), interval: Duration(seconds: 10));
return () {
if (controller.loading.value && controller.data.value == null) {
return CircularProgressIndicator();
}
return Badge(
label: Text('${controller.data.value?.length ?? 0}'),
child: Icon(Icons.notifications),
);
};
}
}
4. UI & Layout Hooks
Hook / Composable | Description |
---|---|
useTheme |
Returns the current ThemeData from the widget tree. |
useDark |
Returns a reactive boolean Ref that is true if the current theme is dark. |
useWidgetSize |
Reactively provides the Size of a widget using a LayoutBuilder . |
useWidgetBox |
Reactively provides the BoxConstraints of a widget using a LayoutBuilder . |
useStream |
Subscribes to a stream and returns a reactive Ref with its latest value. |
useStreamDefault |
Similar to useStream , but with a default value. |
5. Stream Hooks
Hook / Composable | Description |
---|---|
useStream |
Subscribes to a stream and returns a reactive Ref with its latest value. |
useStreamDefault |
Similar to useStream , but with a default value. |
useStream
Subscribes to a stream and provides its data as a reactive Ref
.
class StreamExample extends KaeruWidget {
@override
Setup setup() {
final stream = Stream.periodic(Duration(seconds: 1), (i) => i);
final value = useStream(stream);
return () => Text('Value: ${value.value ?? 'Loading...'}');
}
}
useStreamDefault
Similar to useStream
, but it uses a default value until the stream provides its first item.
class StreamDefaultExample extends KaeruWidget {
@override
Setup setup() {
final stream = Stream.periodic(Duration(seconds: 1), (i) => i);
final value = useStreamDefault(stream, defaultValue: 0);
return () => Text('Value: ${value.value}');
}
}
5. Notes
- Lifecycle hooks:
onBeforeUnmount
,onUpdated
, etc., are core reactive hooks for cleanup & reactive side effects.
- Controller hooks:
- Auto dispose, ready for reactive binding with Kaeru.
- Widgets with built-in Mixins:
- Enable Flutter core behavior without creating State manually.
- Dynamic delegate pattern:
- Combine multiple delegates (Animation, Scroll, Tab, ValueNotifier) with auto setup & dispose.
✨ For Large Language Models (LLMs)
This repository includes special files to help you understand and generate code for this library.
llm.txt
: A concise guide covering the main concepts and providing a quick-start example. Use this for a general overview.llm_full.txt
: A comprehensive documentation file containing detailed descriptions of every public class, function, and hook. Use this as a full reference for generating detailed or complex code.
When asked to work with this library, refer to these files to ensure your generated code is accurate and idiomatic.
🏗 API Documentation
1️⃣ Reactive State: Ref<T>
Represents a reactive variable that automatically triggers updates when changed.
Parameters:
Parameter | Type | Description |
---|---|---|
value |
T |
The initial value of the reactive reference. |
Methods:
Method | Returns | Description |
---|---|---|
select<U>(U Function(T value)) |
Computed<U> |
Creates a computed value derived from this Ref<T> . |
Example:
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
2️⃣ Derived State: Computed<T>
Creates a computed value that automatically updates when dependencies change.
Parameters:
Parameter | Type | Description |
---|---|---|
getter |
T Function() |
A function that returns the computed value. |
Methods:
Method | Returns | Description |
---|---|---|
select<U>(U Function(T value)) |
Computed<U> |
Creates a derived computed value. |
Example:
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
3️⃣ Effects: watchEffect
& watch
watchEffect(Function callback) -> VoidCallback
- Automatically tracks dependencies and re-executes when values change.
Example:
final stop = watchEffect$(() {
print("Count is now: ${count.value}");
});
count.value++; // ✅ Automatically tracks dependencies
stop(); // ✅ Stops watching
watch$(List<ChangeNotifier> sources, Function callback, {bool immediate = false}) -> VoidCallback
- Watches multiple
ChangeNotifier
sources. - If
immediate = true
, executes the callback immediately.
Example:
final stop = watch$([count], () {
print("Count changed: ${count.value}");
}, immediate: true);
stop(); // ✅ Stops watching
4️⃣ Asynchronous Derived State: AsyncComputed<T>
Handles computed values that depend on asynchronous operations.
Parameters:
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. |
Example:
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"
5️⃣ UI Integration: KaeruMixin
and Watch
KaeruMixin
(StatefulWidget Integration)
Allows stateful widgets to easily integrate with reactive values.
Example:
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"),
),
],
);
}
}
Watch
(Automatic UI Rebuilds)
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'....
}
}
Example:
Watch(
() => Text("Value: ${count.value}"),
)
KaeruBuilder
(Builder for Kaeru)
Example:
KaeruBuilder((context) {
final counter = context.ref(0);
return Watch(() => Text(counter.value.toString()));
});
6️⃣ Integration with ValueNotifier & ChangeNotifier
ValueNotifier.toRef()
Converts a ValueNotifier<T>
into a Ref<T>
.
Example:
final valueNotifier = ValueNotifier(0);
final ref = valueNotifier.toRef();
ref.addListener(() {
print("Updated: ${ref.value}");
});
valueNotifier.value = 10; // ✅ Ref updates automatically
ValueNotifier
Extension
Adds .toRef()
to ValueNotifier
to integrate seamlessly.
7️⃣ Selector: usePick
Creates a computed value that tracks only the selected part of a reactive object, optimizing updates.
Parameters:
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. |
Example:
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};
8️⃣ Cleanup: onWatcherCleanup
Registers a callback to be called when the watcher or computed is refreshed or disposed.
Parameters:
Parameter | Type | Description |
---|---|---|
callback |
VoidCallback |
Function to be called on cleanup/dispose. |
Example:
watchEffect$(() {
// ... reactive code ...
onWatcherCleanup(() {
// cleanup logic here
});
});
// or widget Watch
Watch(() {
onWatcherCleanup(() {
// cleanup logic here
});
////
});
// or Computed
Computed(() {
onWatcherCleanup(() {
// cleanup logic here
});
////
});
9️⃣ Utility: nextTick
Runs a callback after the current microtask queue is flushed (similar to Promise.resolve().then()
in JS).
Parameters:
Function | Description |
---|---|
ref<T>(value) |
Creates a reactive reference. Access/modify its content via the .value property. |
computed<T>(fn) |
Creates a read-only, cached value that is derived from other reactive sources. |
asyncComputed<T>(fn) |
A version of computed for asynchronous operations, returning null until the future completes. |
watchEffect(fn) |
Runs a function immediately and re-runs it automatically whenever any of its reactive dependencies change. |
watch(sources, fn) |
Triggers a callback only when specific sources change. |
prop<T>(fn) |
Creates a reactive Computed property from the parent widget's attributes. |
2. Lifecycle Hooks
Manage your widget's side effects with simple lifecycle functions inside setup()
:
Hook | Description |
---|---|
onMounted(fn) |
Called once after the widget is first inserted into the widget tree. |
onUpdated(fn) |
Called after the widget updates. |
onBeforeUnmount(fn) |
Called just before the widget is removed from the widget tree. Perfect for cleanup. |
onDeactivated(fn) |
Called when the widget is deactivated. |
onActivated(fn) |
Called when the widget is re-inserted into the tree after being deactivated. |
3. Flutter Hooks
Kaeru provides use*
hooks that automatically create and dispose of common Flutter objects.
Hook | Description |
---|---|
useAnimationController() |
Creates and disposes of an AnimationController . |
useTabController() |
Creates and disposes of a TabController . |
useScrollController() |
Creates and disposes of a ScrollController . |
useTextEditingController() |
Creates and disposes of a TextEditingController . |
useFocusNode() |
Creates and disposes of a FocusNode . |
useSingleTickerState() |
Provides a TickerProvider for animations. |
useKeepAliveClient() |
Keeps a widget alive in a list (e.g., ListView ). |
useContext() |
Gets the current BuildContext . |
useWidget<T>() |
Gets the current widget instance. |
🏗 Contributing
Pull requests and feature requests are welcome! Feel free to open an issue or contribute.
📜 License
MIT License. See LICENSE for details.
Libraries
- composables/define_widget/auto_dispose
- composables/define_widget/bus
- composables/define_widget/composables/use_context
- composables/define_widget/composables/use_hover_widget
- composables/define_widget/composables/use_ref
- composables/define_widget/composables/use_state
- composables/define_widget/composables/use_widget_bounding
- composables/define_widget/composables/use_widget_size
- composables/define_widget/define_widget
- composables/define_widget/life
- composables/define_widget/main
- This library is deprecated and will be removed in a future version.
Please use
KaeruWidget
instead. - composables/define_widget/reactivity
- composables/define_widget/widget/define_widget_builder
- composables/exception/no_watcher_found_exception
- composables/next_tick
- composables/on_watcher_cleanup
- composables/shared/get_current_instance
- composables/use_pick
- composables/watch
- composables/watch_effect
- event_bus
- extensions/value_notifier_to_ref
- foundation/async_computed
- foundation/computed
- foundation/prop
- foundation/ref
- kaeru
- A reactive state management library for Flutter, inspired by Vue Composition API.
- mixins/kaeru_life_mixin
- mixins/kaeru_listen_mixin
- mixins/kaeru_mixin
- shared/reactive_notifier
- shared/watcher
- widget/kaeru_widget/composables/flutter
- widget/kaeru_widget/composables/use_context
- widget/kaeru_widget/composables/use_dark
- widget/kaeru_widget/composables/use_keep_alive_client
- widget/kaeru_widget/composables/use_request/load_more/controller
- widget/kaeru_widget/composables/use_request/load_more/load_more
- widget/kaeru_widget/composables/use_request/pagination/controller
- widget/kaeru_widget/composables/use_request/pagination/options
- widget/kaeru_widget/composables/use_request/pagination/pagination
- widget/kaeru_widget/composables/use_request/polling/polling
- widget/kaeru_widget/composables/use_request/request/controller
- widget/kaeru_widget/composables/use_request/request/options
- widget/kaeru_widget/composables/use_request/request/request
- widget/kaeru_widget/composables/use_request/use_request
- widget/kaeru_widget/composables/use_restoration
- widget/kaeru_widget/composables/use_single_ticker_state
- widget/kaeru_widget/composables/use_stream
- widget/kaeru_widget/composables/use_theme
- widget/kaeru_widget/composables/use_widget
- widget/kaeru_widget/composables/use_widget_box
- widget/kaeru_widget/composables/use_widget_size
- widget/kaeru_widget/kaeru_widget
- widget/kaeru_widget/life
- widget/kaeru_widget/reactive
- widget/kareu_builder
- widget/watch