06 February 2022

Reactive State Management with Restate

A guide to implementing Reactive state management in Flutter.

Dan Reynolds
Dan Reynolds @thederivative

Restate is a reactive state management libray for Flutter applications with no dependencies and < 200 lines.

Flutter has a variety of great state management libraries you can explore to get a feel for what works best for your own applications. With Restate, the goal was to make a state management lib that was simple to use and as frictionless as possible.

In the past, we’ve used state management tools like InheritedWidgets or Redux, which have their own powerful features, but also come with more boilerplate and can be higher friction to use. Let’s dive in and see how Restate compares!

Each Restate StateBloc holds a single state value accessible synchronously, as well as a Future or as a Stream of values.

  • StateBloc.value - Returns the current state value synchronously.
  • StateBloc.current - Returns a Future that resolves with the current value if it already has a value or otherwise waits for one to be added.
  • StateBloc.stream - Returns a Stream of updates to the state value.
  • StateBloc.changes - Returns a Stream of changes to the state value including the current and previous value.

Reading the current value

import 'package:restate/restate.dart';

final counterState = StateBloc<int>(0);
print(counterState.value); // 0
counterState.add(1);
print(counterState.value); // 1

Listening to a Stream of values

import 'package:restate/restate.dart';

final counterState = StateBloc<int>(0);

counterState.stream.listen((value) {
  print(value);
  // 0
  // 1
  // 2
});

counterState.add(1);
counterState.add(2);

Listening to a Stream of changes

import 'package:restate/restate.dart';

final counterState = StateBloc<int>(0);

counterState.changes.listen((value) {
  print('${value.previous}->${value.current}');
  // null->0
  // 0->1
  // 1->2
});

counterState.add(1);
counterState.add(2);

Waiting for the current value

import 'package:restate/restate.dart';

final counterState = StateBloc<int>();

counterState.current.then((value) => print(value)); // 1
counterState.add(1);
counterState.current.then((value) => print(value)); // 1

Accessing State in Widgets

Accesing and listening for updates to your state is as simple as creating a StateBloc and then using a StreamBuilder to rebuild your widget when data changes:

final counterStateBloc = StateBloc<int>(0);

class MyWidget extends StatelessWidget {
  @override
  build(context) {
    return StreamBuilder(
      stream: counterStateBloc.stream,
      builder: (context, counterSnap) {
        if (!counterSnap.hasData) {
          return Text('Waiting for value...');
        }

        final counter = counterSnap.data;

        return Column(
          children: [
            Text('Counter: $counter'),
            ElevatedButton(
              onPressed: () {
                counterStateBloc.add(counter + 1);
              },
            ),
          ],
        );
      }
    )
  }
}

That’s it! You can run the demo to see a more in-depth working example.

Updating a StateBloc value

Generally, the StateBloc.add API is sufficient for updating the current value held by a StateBloc. Sometimes, however, you may be working with complex objects that need to be mutated.

To keep your state values immutable, you can see if it’s possible to use a copyWith function to your objects:

class User {
  String firstName;
  String lastName;

  User({
    required this.firstName,
    required this.lastName,
  });

  User copyWith({
    String? firstName,
    String? lastName,
  }) {
    return User(
      firstName: firstName ?? this.firstName,
      lastName: lastName ?? this.lastName,
    );
  }
}

final user = User(firstName: 'Anakin', lastName: 'Skywalker');
final userState = UserStateBloc<User>(user);

userState.add(
  userState.value.copyWith(
    firstName: 'Darth',
    lastName: 'Vader',
  ),
);

Many Flutter data objects like TextStyle already support this pattern.

If you instead need to mutate the current value, you can use the StateBloc.setValue API:

final user = User(firstName: 'Anakin', lastName: 'Skywalker');
final userState = UserStateBloc<User>(user);

userState.setValue((currentUser) {
  currentUser.firstName = 'Darth';
  currentUser.lastName = 'Vader';
});

The setValue API provides the current value held by the StateBloc, allowing you to mutate it as necessary, and then re-emits that object on the StateBloc.stream.

Feedback Welcome

Let us know if there’s a feature or changes you would like to see to Restate on GitHub and happy coding!

Categories

Flutter Library