reactive_state 0.7.0-dev.1

  • Readme
  • Changelog
  • Example
  • Installing
  • 56

reactive_state #

Pub Build Status

Easy to understand reactive state management for Flutter apps and for writing reusable Flutter components.

Principles #

Observable state #

State is held in one or multiple instances of Value or similar classes implementing ValueNotifier. These are standard Flutter interfaces that everybody knows from TextEditingController, Animation, etc.

Additionally, you can use ListValue and MapValue for creating observable List and Map values that can notify you about fine-grained change events (instead of the whole value changing).

Reactive widgets #

AutoBuild automatically rebuilds your widgets when a ValueNotifier (or any Listenable) triggers a notification. It's similar to Flutter's ValueListenableBuilder, but it can track multiple dependencies and also works with Listenable.

No need to call addListener/removeListener. Just get() the value directly while AutoBuild takes care of tracking your dependencies.

Unlike InheritedWidget and Provider you get fine-grained control over what gets rebuilt.

Standard Flutter classes like TextEditingController and Animation implement ValueListenable and thus work nicely with AutoBuild.

Derived/computed state #

DerivedValue is an observable value that is computed (derived) from other observable values.

Also, ListValue and MapValue provide .map() and other operations for creating derived containers that keep themselves updated on a per-element basis.

Less boilerplate and indirection #

The resulting code is much simpler than the same solution in BLoC or Redux.

  • No streams, no StreamBuilder, no asynchronous loading of widgets (unless you really need it).
  • No special event objects, no event handlers with long switch() statements.

Usage #

Note: Also see reference for details.

A simple AutoBuild example:

import 'package:flutter/material.dart';
import 'package:reactive_state/reactive_state.dart';

class MyPage extends StatelessWidget {
  MyPage({Key key, @required this.counter}) : super(key: key);

  final ValueNotifier<int> counter;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Column(
        children: <Widget>[
          AutoBuild(builder: (context, get, track) {
            return Text('Counter: ${get(counter)}');
          }),
          MaterialButton(
            onPressed: () => counter.value++,
            child: Text('Increment'),
          ),
        ],
      ),
    );
  }
}

Note that in real-world applications you shouldn't directly mutate the state, but instead put that into separate methods e.g. on an object made accessible through the provider package.

Also, take a look at the example in the repo.

autorun and AutoRunner #

Outside of widgets you might still want to react to state changes. You can do that with autorun() and AutoRunner (see reference for details).

Value vs ValueNotifier #

As an alternative to ValueNotifier you can also use reactive_state's Value class which provides an update() method for modifying more complex objects:

class User {
  String name = '';
  String email = '';
  // ...
}

var userValue = Value(User());
userValue.update((user) {
  user.name = 'Adam';
  user.email = 'adam@adam.com';
});

This is similar to calling setState() with StatefulWidget. With update() you can change multiple attributes and Value will trigger a single notification once finished - even if nothing was changed (so you don't need to implement comparison operators for complex objects).

DerivedValue #

DerivedValue is a dynamically calculated ValueListenable that updates its value whenever its dependencies change:

var user = Value(User());
var emailLink = DerivedValue((get, track) => 'mailto:${get(user).email}');

Here, emailLink can be observed on its own and is updated whenever user is modified.

ListValue and MapValue #

A simple example showing a few things that can be done:

final listValue = ListValue(<int>[]);
final mappedList = listValue.map((x) => x.toString());
final listToMap = mappedList.toMap((x) => MapEntry(2 * int.parse(x), x));
final invertedMap = listToMap.map((k, v) => MapEntry(v, k));

listValue.addAll([4, 1]);
// => invertedMap.value == {'4': 8, '1': 2}

Changelog #

[0.7.0-dev.1] #

ATTENTION: This release comes with a few breaking changes.

  • Renamed package to reactive_state (I'm sorry, but the old name gave people the impression that this is intended for simple apps)
  • Renamed AutoRebuild to AutoBuild
  • Added ListValue and MapValue which allow observing individual change events
  • Added .map() and other operations allowing to create derived observable lists and maps more efficiently than with DerivedValue

[0.6.0] #

  • Added DerivedValue, AutoRunner, autorun
  • Upgraded to Dart 2.7

[0.5.1+2] #

[0.5.1+1] #

  • Initial public release

example/lib/main.dart

// This is a simple, but pretty complete reactive_state example.
// It demonstrates use-cases for small and large apps:
//
// Actions & state values can be globally accessible via Provider.
// This can be useful for making e.g. the currently logged-in user accessible
// from anywhere in the app.
//
// Actions & state values can alternatively be passed directly as arguments.
// This is important for writing reusable components that mustn't depend on
// some globally accessible app-specific state.

import 'package:flutter/material.dart';
import 'package:reactive_state/reactive_state.dart';
import 'package:provider/provider.dart';

void main() => runApp(App());

class User {
  User({this.name = '', this.email = '', this.location = ''});

  String name;
  String email;
  String location;
}

// Global state that the whole app has access to via a Provider.
// For complex apps you might want to split this up into multiple classes
// that cleanly take care of orthogonal state & action aspects.
class GlobalState {
  // We use Value instead of ValueNotifier because we need the update() helper below.
  final user = Value<User>(User());

  // A globally reachable action that is potentially used in multiple places.
  void updateUser(String name, String email) {
    // This demonstrates the Value.update() helper
    user.update((u) {
      u.name = name;
      u.email = email;
    });
  }
}

// We use a StatefulWidget to retain the state between hot reloads
class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  // Global state that will be available in the whole app.
  final state = GlobalState();

  // Local state that is only accessible to a subsection of the app.
  // If you have to pass lots of these objects as arguments *and* if they
  // conceptually belong together, you might want to group them in classes like
  // we did with GlobalState and pass a whole group as a single argument.
  final counter = ValueNotifier<int>(0);

  // A local action that is potentially used in multiple places, but only
  // needed in a subsection of your app.
  void decrement() {
    counter.value--;
  }

  @override
  Widget build(BuildContext context) {
    // We use a MultiProvider, so we can easily add more state objects later
    return MultiProvider(
      providers: [Provider.value(value: state)],
      child: MaterialApp(
        title: 'reactive_state example',
        home: HomePage(counter: counter, decrement: decrement),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  HomePage({Key key, @required this.counter, @required this.decrement})
      : super(key: key);

  final ValueNotifier<int> counter;
  final VoidCallback decrement;
  final nameController = TextEditingController();
  final emailController = TextEditingController();

  void _saveUser(GlobalState globalState) {
    globalState.updateUser(nameController.text, emailController.text);
  }

  @override
  Widget build(BuildContext context) {
    final globalState = Provider.of<GlobalState>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Edit values'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Counter (press "+"/"-" buttons):'),
            // We use multiple AutoBuild widgets to minimize redrawing.
            AutoBuild(builder: (context, get, track) {
              return Text('${get(counter)}');
            }),
            SizedBox(height: 20),
            Text('User data'),
            AutoBuild(builder: (context, get, track) {
              var user = get(globalState.user);
              return Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text('Name: ${user.name}'),
                  Text('Email: ${user.email}'),
                ],
              );
            }),
            Container(
              constraints: BoxConstraints(
                  maxWidth: MediaQuery.of(context).size.width * 0.7),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  TextFormField(
                    decoration: InputDecoration(hintText: 'Name'),
                    controller: nameController,
                    textCapitalization: TextCapitalization.words,
                    keyboardType: TextInputType.text,
                  ),
                  TextFormField(
                    decoration: InputDecoration(hintText: 'Email'),
                    controller: emailController,
                    autocorrect: false,
                    keyboardType: TextInputType.emailAddress,
                  ),
                ],
              ),
            ),
            AutoBuild(builder: (context, get, track) {
              var enabled = get(nameController).text.isNotEmpty &&
                  get(emailController).text.isNotEmpty;
              var onPressed = enabled ? () => _saveUser(globalState) : null;
              return MaterialButton(
                onPressed: onPressed,
                child: Text('Save'),
              );
            })
          ],
        ),
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              // Demonstrating trivial update case
              onPressed: () => counter.value++,
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              // Demonstrating reusable action via callback
              onPressed: decrement,
            ),
          ),
        ],
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  reactive_state: ^0.7.0-dev.1

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:reactive_state/reactive_state.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
15
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
95
Overall:
Weighted score of the above. [more]
56
Learn more about scoring.

We analyzed this package on Mar 31, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6
  • Flutter: 1.12.13+hotfix.8

Health suggestions

Format lib/src/autorun.dart.

Run flutter format to format lib/src/autorun.dart.

Maintenance suggestions

Package is pre-release. (-5 points)

Pre-release versions should be used with caution; their API can change in breaking ways.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test