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 type T.
  • Error: When an operation fails, carrying a failure object of type F.

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 /example directory.

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:

  1. Fork the repository.
  2. Create a new branch for your feature or bug fix.
  3. Make your changes and ensure they adhere to the project's coding style.
  4. 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.