flowmap 0.0.2

FlowMap #

A state management utility for Flutter that makes it easy to stream, slice, and paint widgets with key/value pairs.

Why? #

The Map - when combined with RxDart - makes it possible to dynamically stream and share data in Flutter without boilerplate/configuration, leading to faster prototyping of complex state relationships when compared to redux, inherited widgets, bloc, etc.

  • Event-driven one-way data flow.
  • Stream data globally or scoped to your widgets.
  • Repaint widgets on specific value changes or actions/events.
  • Time-travel debugging with included devtools widget.
  • No boilerplate and does not take over your architecture.
  • Focused on simplicity and flexibility.

Drawbacks. Values in the map are dynamic and the Actions are created dynamically at runtime, so it's much more implicit than most state management tools.

Quick Start #

  1. Create a FlowMap. 2. Put some widgets its builder function 3. Change values on the map.
// Initialize data globally or scoped to a widget
FlowMap counter = FlowMap(seed: { 'count': 0 });

// somewhere in your widget tree...
// works just like a StreamBuilder, but gives you the current value in the map
    counter.builder(builder: (Build context, Map state) {
        int currentCount = state['count'];
        return FlatButton(
            onPressed: () => counter.update('count', currentCount + 1), 
            child: Text('Count $currentCount')
        );
    })

It has a special widget that allows you to view and "time-travel" across state changes.

// you'll want this in a scrollable view
ListView(
    children: [counter.devtools()]
)

The FlowMap #

The FlowMap is a key/value store similar to a Dart Map, but is treated like an event-driven Stream (similar to Redux under the hood). This means you can listen to data changes and/or events that get dispatched to map.

Create #

Pass an optional seed Map containing default values or start empty.

Keys must be strings. Values can be anything (but I would recommend using types that can be serialized).

FlowMap map = FlowMap(seed: { 'count': 0, 'mantra': 'everything is a stream' });

Read #

You can access the full value as a Stream/Observable or plain Map.

map.stream; // Observable<Map>

map.value; // Map

Or you can listen to individual values by specifying a key. The stream will only emit on distinct changes to the value at this location.

Observable<dynamic> count$ = map.streamItem('count'); // Observable<dynamic>

int count = map.getItem('count'); // dynamic

Updates #

You can mutate the state using the familiar API methods below.

map.update('count', count + 1); // Sets data at this key

map.reset({ Map next }); // Resets to initial seed value, or sets a new Map

map.remove('widget'); // Removes the key/value pair

// provides a context to do anything to the state
map.action(name: 'ADD_NAME', data: 'Jeff', mutation: (Map state, dynamic data) {
    state['name'] = data;
    state['hasName'] = true;
    return state;
})

When you call one of these methods you're actually dispatching an Action. For example, the first method above results in an action named UPDATE-count. Why does this matter? Actions allow us to keep track of every event that happens to the map and the diff. Tip: Use the devtools widget to inspect every action visually.

Actions are synchronous, but you can also mutate the state with the value resolved from a future. This will dispatch two actions (1) START and (2a) SUCCESS and perform an update with the resolved value, or (2b) ERROR.

var future = Future.delayed(Duration(milliseconds: 100)).then((v) => 23)
map.updateAsync('count', future);
// ASYNC-START
// wait 100ms
// ASYNC-SUCCESS and updates the state

If you want a more explicit API, you can initialize a map in your own class and use custom action names.

class Counter {

    final countKey = 'count'; 
    final map FlowMap = FlowMap(seed: { countKey: 0 })

    int get count {
        return map.getItem(countKey);
    }

    // dispatchs INCREMENT-count
    void increment() {
        map.update(countKey, count + 1, name: 'INCREMENT');
    }

    // dispatchs DECREMENT-count
    void decrement() {
        map.update(countKey, count - 1, name: 'DECREMENT');
    }
}

Actions #

Actions are synchronous events that provide a context for modifying the state of the FlowMap. Keep in mind, the FlowMap update methods above create actions for you automatically, so you may never need to mess with them directly.

Actions must be synchronous and should only be concerned with changing values on the state. The name gives the action meaning, the optional data allows you to pass in data from external sources, and the optional mutation is a function that provides a context to create the next state (same as a reducer in redux). The callback provide a the current state, the optional data payload, and requires you to return the next state.

Note: Actions are not required to mutate the state - they can be used simply dispatch events.

Action increment = Action(name: 'INCREMENT', data: null, mutation: (Map state, dynamic data) {
    state['count']++;
    return state;
})

// On some button tap
map.dispatch(increment);

You can listen to the entire stream of actions, or specific actions, to create "reactions" for running side-effects.

map.actions.listen((a) => print(action.name));

map.actionName('INCREMENT-count').listen(() => someSideEffect() );

Build #

The combination of an action stream with the current state is very powerful. You can rebuild widgets by passing the streams to a StreamBuilder and the builder will always have access to the current value on the FlowMap.

StreamBuilder(
    stream: map.stream,
    builder: (context, snap) => Text(value: '${snap.data.count}' ) 
),

This package includes two widgets that wrap StreamBuilder to listen to value changes and actions.

FlowMapBuilder(
    map: map,
    keyName: 'counter',
    builder: (context, state) {
        print('${state}')
        return Text('$state'); 
    } 
),
ActionBuilder(
    map: map,
    actionName: 'UPDATE-counter',
    builder: (context, action) {
        print('${action.name}')
        return Text(map.getItem('count'),toString()); 
    } 
),

Or you can call the builder on the map directly for even more sugar.

map.builder(builder: (context, state) {
    var currentCount = map.getItem('count');
    return FlatButton(
            onPressed: () => map.update('count', currentCount + 1), 
            child: Text('Count $currentCount')
    );
}),

DevTools #

There is a special devtools widget for viewing and time-traveling through state changes.

FlowMapDevTools(map: yourFlowMap);
// or
yourFlowMap.devtools();

// It works nicely in a Drawer with a ListView

return Scaffold(
      drawer: Drawer(child: ListView(shrinkWrap: true, children: [
          yourFlowMap.devtools()
        ])),
)

[0.0.1] - March 10th, 2019

Initial Release

example/README.md

flowmap_example #

A new Flutter project.

Getting Started #

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

For help getting started with Flutter, view our online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.

Use this package as a library

1. Depend on it

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


dependencies:
  flowmap: ^0.0.2

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:flowmap/flowmap.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
0
Health:
Code health derived from static analysis. [more]
75
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
80
Overall:
Weighted score of the above. [more]
39
Learn more about scoring.

We analyzed this package on Aug 18, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.4.0
  • pana: 0.12.19
  • Flutter: 1.7.8+hotfix.4

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Health issues and suggestions

Fix lib/src/widgets.dart. (-25 points)

Analysis of lib/src/widgets.dart failed with 1 error:

line 29 col 47: The name 'Action' is defined in the libraries 'package:flowmap/src/map.dart' and 'package:flutter/src/widgets/actions.dart'.

Format lib/flowmap.dart.

Run flutter format to format lib/flowmap.dart.

Format lib/src/map.dart.

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

Maintenance issues and suggestions

Support latest dependencies. (-10 points)

The version constraint in pubspec.yaml does not support the latest published versions for 1 dependency (rxdart).

Package is pre-v0.1 release. (-10 points)

While nothing is inherently wrong with versions of 0.0.*, it might mean that the author is still experimenting with the general direction of the API.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
rxdart ^0.21.0 0.21.0 0.22.1+1
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.6 1.1.7
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test
test 1.5.3