rail 1.0.0-dev.1
rail: ^1.0.0-dev.1 copied to clipboard
A lightweight Flutter MVVM library for state and side effects management.
Rail #
Rail is a lightweight MVVM-inspired state management library for Flutter, heavily influenced by orbit-mvi and flutter_bloc. It aims to provide a simple, testable, and predictable way to model application state and side-effects.
Usage #
Create a Rail that exposes a typed state and optional effects. Provide it to the widget tree with RailProvider, and read states in the UI with RailBuilder or RailConsumer.
Example:
// counter_effect.dart
sealed class CounterEffect {}
class CongratsMessageEffect extends CounterEffect {
final int count;
CongratsMessageEffect(this.count);
}
// counter_rail.dart
class CounterRail extends Rail<int, CounterEffect> {
CounterRail() : super(initialState: 0);
void increment() {
final newCount = state + 1;
emitState(newCount);
if (newCount % 10 == 0) emitEffect(CongratsMessageEffect(newCount));
}
}
// In your widget tree
RailProvider<CounterRail>(
create: (_) => CounterRail(),
child: Scaffold(
body: Center(
child: RailConsumer<CounterRail, int, CounterEffect>(
listener: (context, effect) {
if (effect is CongratsMessageEffect) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
"Congrats! You pushed the button for incredible ${effect.count} times!",
)));
}
},
builder: (context, count) => Text('Count: $count'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterRail>().increment(),
child: Icon(Icons.add),
),
),
)
For more examples, see the examples folder.
Widgets #
RailBuilder<RAIL, STATE>: rebuilds UI when theRailemits new states.RailListener<RAIL, EFFECT>: listen-only widget for reacting to effects.RailConsumer<RAIL, STATE, EFFECT>: combines state-driven building and effect-driven listening.
Testing #
Rail is designed for testability. Example unit test:
test('counter increments', () async {
final rail = CounterRail();
expect(rail.state, 0);
rail.increment();
expect(rail.state, 1);
await rail.close();
});
Testing your widgets with a real Rail is also simple:
void main() {
late CounterRail rail;
setUp(() {
rail = CounterRail();
});
tearDown(() {
rail.close();
});
testWidgets("Should increment on button tap", (tester) async {
await tester.pumpWidget(MaterialApp(
home: RailProvider(
create: (context) => rail,
child: const CounterPage(),
),
));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('1'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('2'), findsOneWidget);
});
}
Alternatively, you can mock your Rail using a lib like mockito:
import 'counter_page_test.mocks.dart';
@GenerateNiceMocks([MockSpec<CounterPageRail>()])
void main() {
testWidgets("Should update counter text with rail state", (widgetTester) async {
final rail = MockCounterPageRail();
when(rail.state).thenReturn(2);
await widgetTester.pumpWidget(MaterialApp(
home: RailProvider<CounterPageRail>(
create: (context) => rail,
child: const CounterPage(),
),
));
expect(find.text('2'), findsOneWidget);
});
}
For more testing examples, see the example tests folder.
Contributing #
- Read the existing tests and examples in
example/andtest/before adding new features. - Open issues for bug reports or feature requests.
- Follow the repository coding style and include tests for new behavior.