FlowDux Flutter
Flutter bindings for FlowDux state management library.
Features
- StoreProvider - Provide store to widget tree via InheritedWidget
- StoreScope - Lifecycle-aware provider that creates/closes a store automatically
- StoreBuilder - Rebuild widgets when state changes
- StoreSelector - Optimized rebuilds with selectors
- StoreConsumer - Access both store and state
- StoreListener - Side effects on state changes
Installation
dependencies:
flowdux: ^0.3.2
flowdux_flutter: ^0.3.0
Quick Start
1. Wrap Your App with StoreProvider
void main() {
final store = createStore<AppState, AppAction>(
initialState: AppState(),
reducer: appReducer,
middlewares: [AppMiddleware()],
);
runApp(
StoreProvider<AppState, AppAction>(
store: store,
child: MyApp(),
),
);
}
2. Use StoreBuilder for Reactive UI
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreBuilder<AppState, AppAction>(
builder: (context, state) {
return Text('Count: ${state.count}');
},
);
}
}
3. Dispatch Actions
// Using context extension
ElevatedButton(
onPressed: () => context.dispatch<AppState, AppAction>(IncrementAction()),
child: Text('Increment'),
)
// Or access the store directly
ElevatedButton(
onPressed: () {
final store = StoreProvider.of<AppState, AppAction>(context);
store.dispatch(IncrementAction());
},
child: Text('Increment'),
)
Widgets
StoreProvider
Provides a store to all descendant widgets using InheritedWidget.
StoreProvider<AppState, AppAction>(
store: store,
child: MyApp(),
)
// Access the store
final store = StoreProvider.of<AppState, AppAction>(context);
// Or use the extension
final store = context.store<AppState, AppAction>();
// Safe access (returns null if not found)
final store = StoreProvider.maybeOf<AppState, AppAction>(context);
StoreScope
Owns a store's lifecycle: creates it once via create and closes it on dispose.
Use this anywhere the build function may run multiple times — e.g. inside a
PageRoute's pageBuilder, a custom route builder, or a NestedRoute. Without
it, building a fresh Store inline causes the store to be replaced on rebuild
while child State objects keep stale references, dropping dispatched actions
silently.
PageRouteBuilder(
pageBuilder: (_, __, ___) => StoreScope<AppState, AppAction>(
create: () => createStore<AppState, AppAction>(
initialState: AppState(),
reducer: appReducer,
),
child: MyScreen(),
),
);
The created store is exposed to descendants via StoreProvider, so all the
widgets below (StoreBuilder, StoreSelector, StoreConsumer,
StoreListener, context.store, context.dispatch) work unchanged.
StoreBuilder
Rebuilds its child when the store's state changes.
StoreBuilder<AppState, AppAction>(
builder: (context, state) {
return Column(
children: [
Text('Count: ${state.count}'),
Text('Name: ${state.name}'),
],
);
},
)
You can also pass a store directly:
StoreBuilder<AppState, AppAction>(
store: myStore, // Optional, uses provider if not specified
builder: (context, state) {
return Text('Count: ${state.count}');
},
)
StoreSelector
Optimized widget that only rebuilds when the selected value changes.
StoreSelector<AppState, AppAction, int>(
selector: (state) => state.count,
builder: (context, count) {
return Text('Count: $count');
},
)
// Complex selector
StoreSelector<AppState, AppAction, String>(
selector: (state) => '${state.firstName} ${state.lastName}',
builder: (context, fullName) {
return Text('Name: $fullName');
},
)
StoreConsumer
Combines StoreBuilder with store access for dispatching actions.
StoreConsumer<AppState, AppAction>(
builder: (context, store, state) {
return Column(
children: [
Text('Count: ${state.count}'),
ElevatedButton(
onPressed: () => store.dispatch(IncrementAction()),
child: Text('Increment'),
),
],
);
},
)
With listener for side effects:
StoreConsumer<AppState, AppAction>(
listener: (context, store, state) {
if (state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error!)),
);
}
},
builder: (context, store, state) {
return MyWidget(state: state);
},
)
StoreListener
Listens to state changes without rebuilding. Use for side effects like navigation or showing dialogs.
StoreListener<AppState, AppAction>(
listener: (context, store, state) {
if (state.shouldNavigate) {
Navigator.of(context).pushNamed('/next');
}
},
child: MyWidget(),
)
With conditional listening:
StoreListener<AppState, AppAction>(
listenWhen: (previous, current) => previous.route != current.route,
listener: (context, store, state) {
Navigator.of(context).pushNamed(state.route);
},
child: MyWidget(),
)
Complete Example
import 'package:flutter/material.dart';
import 'package:flowdux_flutter/flowdux_flutter.dart';
// State
class CounterState {
final int count;
CounterState({this.count = 0});
CounterState copyWith({int? count}) =>
CounterState(count: count ?? this.count);
}
// Actions
class IncrementAction implements Action {}
class DecrementAction implements Action {}
// Reducer
final counterReducer = buildReducer<CounterState, Action>((b) {
b.on<IncrementAction>((state, _) => state.copyWith(count: state.count + 1));
b.on<DecrementAction>((state, _) => state.copyWith(count: state.count - 1));
});
void main() {
final store = createStore<CounterState, Action>(
initialState: CounterState(),
reducer: counterReducer,
);
runApp(
StoreProvider<CounterState, Action>(
store: store,
child: MaterialApp(
home: CounterPage(),
),
),
);
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: StoreBuilder<CounterState, Action>(
builder: (context, state) {
return Text(
'${state.count}',
style: Theme.of(context).textTheme.headlineLarge,
);
},
),
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
onPressed: () =>
context.dispatch<CounterState, Action>(IncrementAction()),
child: Icon(Icons.add),
),
SizedBox(height: 8),
FloatingActionButton(
onPressed: () =>
context.dispatch<CounterState, Action>(DecrementAction()),
child: Icon(Icons.remove),
),
],
),
);
}
}
Testing
testWidgets('counter increments', (tester) async {
final store = createStore<CounterState, Action>(
initialState: CounterState(),
reducer: counterReducer,
);
await tester.pumpWidget(
MaterialApp(
home: StoreProvider<CounterState, Action>(
store: store,
child: CounterPage(),
),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.runAsync(() => Future.delayed(Duration(milliseconds: 50)));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
API Reference
See the API documentation for complete details.
License
Apache License 2.0 - see LICENSE for details.
Libraries
- flowdux_flutter
- Flutter bindings for FlowDux state management library.