Flutter Stream Flow 🌀
A simple and lightweight state management utils for Flutter that embraces the power of native Dart Streams and RxDart.
Drop a ⭐ to support the project!
This package allows you to use the native stream API from the Dart language, alongside powerful RxDart operators, to create a predictable and reactive state management solution.
Note
This isn’t a new concept. It simply leverages streams and well-established patterns to promote good practices, consistency, and predictability.
Getting Started
Add flutter_stream_flow to your pubspec.yaml file:
dependencies:
flutter_stream_flow: <latest_version>
or run:
flutter pub add flutter_stream_flow
Then, run flutter pub get.
Core Concepts
flutter_stream_state is built around a few core components:
ViewModel
The ViewModel is the heart of your business logic. It's an abstract class that you extend to manage the state of a specific feature or screen. It exposes a stream of UiState that the UI can listen to.
UiState
UiState<T, F> is a freezed class that represents the state of your UI. It's a union of four possible states:
Idle: The initial state, before any action has been taken.Loading: When an asynchronous operation is in progress.Success: When an operation completes successfully, carrying the data of typeT.Error: When an operation fails, carrying a failure object of typeF.
FlowUiStateBuilder
FlowUiStateBuilder is a widget that listens to a Stream<UiState> from a ViewModel and rebuilds the UI in response to new states. It provides a builder function where you can define the UI for each state.
Usage
Here's a simple example of how to use flutter_stream_state to create a counter that increments asynchronously.
1. Create your ViewModel
Create a class that extends ViewModel and implement the business logic for your feature.
class GalleryViewModel extends ViewModel<DomainMedia> {
final GalleryMediaRepository _repository;
GalleryViewModel(this._repository);
late StreamSubscription _subscription;
void getGalleryData() {
_subscription =
_repository.getAllMedias() // exposes a stream
.delay(const Duration(milliseconds: 500)) // add a delay (just as demonstration)
.listen(
(result) { // It returns a Result<Success, Failure> (e.g.: fpdart or result_dart packages)
result.fold(
(data) => updateState(UiState.success(data)),
(failure) => updateState(UiState.error(failure)),
);
},
onError: (error) => updateState(UiState.error(error)),
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
2. Build your UI
Use the FlowUiStateBuilder to listen to the ViewModel's state stream and build your UI.
import ...;
class GalleryView extends StatefulWidget {
final GalleryViewModel viewModel;
const GalleryView({super.key, required this.viewModel});
@override
State<GalleryView> createState() => _GalleryViewState();
}
class _GalleryViewState extends State<GalleryView> {
GalleryViewModel get viewModel => widget.viewModel;
@override
void initState() {
super.initState();
viewModel.getGalleryData();
}
@override
void dispose() {
viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: FlowUiStateBuilder(
stream: viewModel.uiState$,
builder: (_, state) {
return state.maybeWhen(
success: (items) {
return GridView.builder(
gridDelegate: ...,
itemBuilder: (_, index) => ItemCard(item: items[index]),
itemCount: items.length,
);
},
error: (failure) => PullToRefreshErrorView(...),
orElse: () => const LoadingShimmer(),
);
},
),
);
}
}
This example demonstrates how to create a reactive UI that updates based on the state managed by the ViewModel.
The ViewModel handles the business logic, and the View is responsible for rendering the UI based on the current state.
For a complete and working example, head to the
/exampledirectory.
3. Using FlowBuilder and StreamBuilder
While FlowUiStateBuilder is convenient for handling UiState, you can also use FlowBuilder (from this package) or the standard StreamBuilder (from the Flutter SDK) to listen to any stream from your ViewModel. This is useful when you have streams that don't represent a full UI state (e.g., a simple counter, a theme switch, etc.).
Here's an example of how you could use FlowBuilder to listen to a simple Stream<int> from a ViewModel:
ViewModel
class CounterViewModel extends ViewModel<MyData, MyFailure> {
final _counter = 0.toStream();
Stream<int> get counter$ => _counter.stream;
void increment() {
_counter.add(_counter.value + 1);
}
@override
void dispose() {
_counter.close();
super.dispose();
}
}
View
class MyCounterWidget extends StatelessWidget {
final CounterViewModel viewModel;
const MyCounterWidget({super.key, required this.viewModel});
@override
Widget build(BuildContext context) {
return Column(
children: [
FlowBuilder(
stream: viewModel.counter$,
builder: (_, count) {
return Text('Count: $count');
},
),
ElevatedButton(
onPressed: () => viewModel.increment(),
child: Text('Increment'),
),
],
);
}
}
You can achieve the same result with Flutter's built-in StreamBuilder:
StreamBuilder(
stream: viewModel.counter$,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Count: ${snapshot.data}');
}
return Text('Count: 0');
},
),
Choose the one that best fits your needs. FlowBuilder is slightly more concise if you don't need to handle the ConnectionState and just want to build your widget with the latest data.
How to Contribute
Contributions are welcome to the library! If you have suggestions for improvements, bug reports, or want to add new features, please follow these steps:
- Fork the repository.
- Create a new branch for your feature or bug fix.
- Make your changes and ensure they adhere to the project's coding style.
- Submit a pull request with a clear description of your changes.
Or open a new issue on the Github and I'll look into it.