navhost_state 0.1.0
navhost_state: ^0.1.0 copied to clipboard
Reactive state management extensions for navhost — .obs values, Obs auto-tracking widget, ViewModelBuilder, and Listen.
navhost_state #
Reactive state management for navhost.
navhost_state brings GetX-style .obs reactive values and Obs auto-tracking widgets to Flutter, along with scoped ViewModel management tied to the widget lifecycle.
Features #
.obsreactive values — wrap any value inRx<T>with0.obs,'hello'.obs,false.obsObsauto-tracking widget — rebuilds only when theRxvalues read inside it change- Fine-grained rebuilds — each
Obstracks its own dependencies independently - Scoped ViewModels —
context.viewModel()creates once, reuses across rebuilds, auto-cleans on unmount ViewModelScope— ties ViewModel lifecycle to the widget treerxRoutes()— wraps all navhost routes withViewModelScopeautomaticallyViewModelBuilder— convenience widget that creates + subscribes in one stepListen— subscribes to an existing ViewModel without re-creating it
Getting started #
dependencies:
navhost: ^0.1.0
navhost_state: ^0.1.0
import 'package:navhost/navhost.dart';
import 'package:navhost_state/navhost_state.dart';
Usage #
Reactive values with .obs and Obs #
final count = 0.obs;
// Obs rebuilds automatically when count.value changes
Obs(() => Text('${count.value}'))
// Only notifies when value actually changes
count.value = 5; // triggers rebuild
count.value = 5; // no-op — same value
Multiple Obs watching the same value #
final name = 'World'.obs;
Column(children: [
Obs(() => Text('Hello, ${name.value}!')), // rebuilds on change
Obs(() => Text('Length: ${name.value.length}')), // also rebuilds
])
Each Obs tracks only the Rx values it actually reads — unrelated changes don't cause rebuilds.
ViewModel with .obs #
ViewModels don't need to extend ChangeNotifier. Plain classes work perfectly with .obs + Obs:
class CounterViewModel {
final count = 0.obs;
void increment() => count.value++;
}
ViewModelBuilder and Listen still require ChangeNotifier because they use ListenableBuilder internally. For plain class ViewModels, use context.viewModel() + Obs instead.
Scoped ViewModel with rxRoutes #
rxRoutes() wraps each route with a ViewModelScope, so context.viewModel() just works:
final nav = NavController(
routes: rxRoutes([
NavRoute('/', (_) => const CounterPage()),
NavRoute('/detail/:id', (p) => DetailPage(id: p['id']!)),
]),
);
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.viewModel(() => CounterViewModel());
return Obs(() => Column(
children: [
Text('Count: ${vm.count.value}'),
FilledButton(
onPressed: vm.increment,
child: const Text('Increment'),
),
],
));
}
}
The ViewModel is created once and reused across rebuilds. It is disposed when the route is removed from the navigation stack.
Manual ViewModelScope #
You can use ViewModelScope directly without rxRoutes():
NavRoute(
'/counter',
(_) => ViewModelScope(
child: CounterPage(),
),
)
ViewModelBuilder #
Creates a scoped ViewModel and subscribes to it in one widget:
ViewModelBuilder<CounterViewModel>(
factory: () => CounterViewModel(),
builder: (context, vm, child) => Obs(() =>
Text('Count: ${vm.count.value}'),
),
)
Listen #
Subscribes to an existing ViewModel without re-creating it:
Listen<CounterViewModel>(
builder: (context, vm) => Text('Count: ${vm.count}'),
)
Without navhost — plain StatefulWidget #
The reactive system works independently of navhost. You can use .obs and Obs anywhere:
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
final count = 0.obs;
@override
Widget build(BuildContext context) {
return Obs(() => Text('${count.value}'));
}
}
Example #
See the example app for demos of all features: reactive counter, ViewModelBuilder, multiple observers with a color picker, and a todo list.
License #
MIT