flowdux_flutter 0.2.4
flowdux_flutter: ^0.2.4 copied to clipboard
Flutter bindings for FlowDux state management library. Provides StoreProvider, StoreBuilder, and StoreConsumer widgets.
FlowDux Flutter #
Flutter bindings for FlowDux state management library.
Features #
- StoreProvider - Provide store to widget tree via InheritedWidget
- 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.2.4
flowdux_flutter: ^0.2.4
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);
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.