Skip to content

Commit

Permalink
0.2.0: Added afterware.
Browse files Browse the repository at this point in the history
  • Loading branch information
RedBrogdon committed Oct 18, 2018
1 parent 487abeb commit 7118941
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 95 deletions.
24 changes: 17 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
## [0.1.0] - 10/17/2018.
## [0.2.0] - 10/18/2018

* Added FirstBuildDispatcher, a new widget that will dispatch an action to an
ancestor Store the first time it's built.
* Breaking change: Added the concept of "afterware" to the library.
- Afterware are middle-ware like functions that are invoked after an
`Action` has passed the reducing stage. If you need to perform a
side effect after the app state has been updated in response to a
given `Action` (e.g. save state to disk, dispatch other actions),
afterware is the place to do it.

## [0.0.7] - 10/7/2018.
## [0.1.0] - 10/17/2018

* Began using Dart versioning correctly.
* Added `FirstBuildDispatcher`, a new widget that will dispatch an
`Action` to an ancestor `Store` the first time it's built.

## [0.0.7] - 10/7/2018

* Changed `StoreProvider` to always use `inheritFromWidgetOfExactType`.
* Added `DispatchSubscriber`, a widget that subscribes to an ancestor
`StoreProvider`'s dispatch function and builds widgets that can call
it.

## [0.0.6] - 10/5/2018.
## [0.0.6] - 10/5/2018

* Added `useful_blocs.dart` to hold some built-in `Bloc`s that devs
might want to use.
Expand All @@ -19,11 +29,11 @@
`Action` that has been given to its `afterward` method will also be
cancelled.

## [0.0.5] - 9/11/2018.
## [0.0.5] - 9/11/2018

* Added `afterward` method to the `Action` class.

## [0.0.4] - 8/27/2018.
## [0.0.4] - 8/27/2018

* First release in which I remembered to update the change log.
* Two examples in place, plus the library itself.
Expand Down
39 changes: 28 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,24 @@ store. Rather than using functional programming techniques to compose
reducers and middleware from parts and wire everything up, however, it
uses BLoCs.

![img](https://i.imgur.com/aMuwpWS.png)

The store defines a dispatch stream that accepts new actions and
produces state objects in response. In between, BLoCs are wired into
the stream to function as middleware and reducers. The stream for a
simple, two-BLoC store might look like this, for example:
the stream to function as middleware, reducers, and afterware. Afterware
is essentially a second chance for Blocs to perform middleware-like
tasks *after* the reducers have had their chance to update the app
state. The stream for a simple, two-BLoC store might look like this, for
example:

```
Dispatch ->
BloC #1 middleware ->
BLoC #2 middleware ->
the action is converted into an accumulator (action and state together) ->
BLoC #1 reducer ->
BLoC #2 reducer ->
resulting state object is emitted by store
resulting state object is emitted by store ->
BLoC #1 reducer ->
BLoC #2 reducer ->
```

There are two ways to implement BLoCs. The first is a basic
Expand All @@ -56,10 +59,10 @@ other streamy goodness). The other is an abstract class,
`SimpleBloc<StateType>`, that hides away interaction with the stream and
provides a simple, functional interface.

Middleware methods can perform side effects like calling out to REST
endpoints and dispatching new actions, but reducers should work as pure
functions in keeping with Redux core principles. Middleware are also
allowed to cancel actions.
Middleware and afterware methods can perform side effects like calling
out to REST endpoints and dispatching new actions, but reducers should
work as pure functions in keeping with Redux core principles. Middleware
and afterware are also allowed to cancel (or "swallow") actions.

## Why does this exist?

Expand All @@ -73,7 +76,7 @@ for time-travel debugging if interest merits building those sorts of
tools.

Thus I'd like to see if I can combine the two and get the parts I like
from both. Also, it's a chance to test out...
from both. Also, it's a chance to test out some widgets...

## ViewModelSubscriber

Expand All @@ -92,6 +95,20 @@ you've got a list of complicated records, for example, you can use
`ViewModelSubscriber` widgets to avoid rebuilding the entire list just
because one field in one record changed.

## ViewModelSubscriber

`DispatchSubscriber` is a `ViewModelSubscriber` without the view model.
If you have a piece of UI that just needs to dispatch an `Action` and
doesn't need any actual data from the `Store`, `DispatchSubscriber` is
your huckleberry.

## FirstBuildDispatcher

`FirstBuildDispatcher` automatically dispatches an `Action` to an
ancestor `Store` the first time it's built. If you'd like to refresh
some data from the network any time a particular widget is displayed,
for example, `FirstDispatchBuilder` can help.

## Basic example

Imagine an app that just needs to track a list of `int` as its only
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
</dict>
</plist>
17 changes: 16 additions & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,29 @@ class DescriptionBloc extends SimpleBloc<AppState> {

/// Logs each incoming action.
class LoggerBloc extends SimpleBloc<AppState> {
AppState lastState;

@override
Future<Action> middleware(dispatcher, state, action) async {
print('${action.runtimeType} dispatched. State: $state.');
print('${action.runtimeType} dispatched.');

// This is just to demonstrate that middleware can be async. In most cases,
// you'll want to cancel or return immediately.
return await Future.delayed(Duration.zero, () => action);
}

@override
FutureOr<Action> afterware(DispatchFunction dispatcher, AppState state,
Action action) {
if (state != lastState) {
print('State just became: $state');
lastState = state;
}

return action;
}


}

/// Limits each of the three values in [AppState] to certain maximums. This
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.1.0"
version: "0.2.0"
rxdart:
dependency: transitive
description:
Expand Down
115 changes: 58 additions & 57 deletions lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,21 @@ import 'package:meta/meta.dart';
import 'package:rxdart/subjects.dart' show BehaviorSubject;

/// A Redux-style action. Apps change their overall state by dispatching actions
/// to the [Store], where they are acted on by middleware and reducers. Apps can
/// use [afterward] to specify an [Action] that should be dispatched after the
/// current one is reduced.
/// to the [Store], where they are acted on by middleware, reducers, and
/// afterware in that order.
abstract class Action {
Action _next;

Action();

void afterward(Action a) {
if (_next == null) {
_next = a;
} else {
_next.afterward(a);
}
}

const Action();
factory Action.cancelled() => _CancelledAction();
}

/// An action that middleware methods can return in order to cancel (or
/// "swallow") an action already dispatched to their [Store]. Because rebloc
/// uses a stream to track [Actions] through the dispatch->middleware->reducer
/// pipeline, a middleware method should return something. By returning an
/// instance of this class (and making sure that none of their middleware or
/// reducer methods attempt to catch and act on it), a developer can in effect
/// cancel actions via middleware.
/// An action that middleware and afterware methods can return in order to
/// cancel (or "swallow") an action already dispatched to their [Store]. Because
/// rebloc uses a stream to track [Actions] through the
/// dispatch->middleware->reducer pipeline, a middleware/afterware method should
/// return something. By returning an instance of this class (which is private
/// to this library), a developer can in effect cancel actions via middleware.
class _CancelledAction extends Action {
@override
void afterward(Action a) {}
const _CancelledAction();
}

/// A function that can dispatch an [Action] to a [Store].
Expand All @@ -60,25 +46,26 @@ class Accumulator<S> {
Accumulator<S> copyWith(S newState) => Accumulator<S>(this.action, newState);
}

/// The context in which a middleware function executes.
/// The context in which a middleware or afterware function executes.
///
/// In a manner similar to the streaming architecture used for reducers, [Store]
/// offers each [Bloc] the chance to apply middleware functionality to incoming
/// [Actions] by listening to the "dispatch" stream, which is of type
/// `Stream<MiddlewareContext<S>>`.
/// offers each [Bloc] the chance to apply middleware and afterware
/// functionality to incoming [Actions] by listening to the "dispatch" stream,
/// which is of type `Stream<WareContext<S>>`.
///
/// Middleware functions can examine the incoming [action] and current [state]
/// of the app, and dispatch new [Action]s using [dispatcher]. Afterward, they
/// should emit a new [MiddlewareContext] for the next [Bloc].
class MiddlewareContext<S> {
/// Middleware and afterware functions can examine the incoming [action] and
/// current [state] of the app and perform side effects (including dispatching
/// new [Action]s using [dispatcher]. Afterward, they should emit a new
/// [WareContext] for the next [Bloc].
class WareContext<S> {
final DispatchFunction dispatcher;
final S state;
final Action action;

const MiddlewareContext(this.dispatcher, this.state, this.action);
const WareContext(this.dispatcher, this.state, this.action);

MiddlewareContext<S> copyWith(Action newAction) =>
MiddlewareContext<S>(this.dispatcher, this.state, newAction);
WareContext<S> copyWith(Action newAction) =>
WareContext<S>(this.dispatcher, this.state, newAction);
}

/// A store for app state that manages the dispatch of incoming actions and
Expand All @@ -89,20 +76,23 @@ class MiddlewareContext<S> {
/// - Create a controller for the dispatch/reduce stream using an [initialState]
/// value.
/// - Wire each [Bloc] into the dispatch/reduce stream by calling its
/// [applyMiddleware] and [applyReducers] methods.
/// [applyMiddleware], [applyReducers], and [applyAfterware] methods.
/// - Expose the [dispatcher] by which a new [Action] can be dispatched.
class Store<S> {
final _dispatchController = StreamController<MiddlewareContext<S>>();
final _dispatchController = StreamController<WareContext<S>>();
final _afterwareController = StreamController<WareContext<S>>();
final BehaviorSubject<S> states;

Store({
@required S initialState,
List<Bloc<S>> blocs = const [],
}) : states = BehaviorSubject<S>(seedValue: initialState) {
var dispatchStream = _dispatchController.stream.asBroadcastStream();
var afterwareStream = _afterwareController.stream.asBroadcastStream();

for (Bloc<S> bloc in blocs) {
dispatchStream = bloc.applyMiddleware(dispatchStream);
afterwareStream = bloc.applyAfterware(afterwareStream);
}

var reducerStream = dispatchStream.map<Accumulator<S>>(
Expand All @@ -115,39 +105,37 @@ class Store<S> {
reducerStream.listen((a) {
assert(a.state != null);
states.add(a.state);
if (a.action._next != null) {
dispatcher(a.action._next);
}
_afterwareController.add(WareContext<S>(dispatcher, a.state, a.action));
});

// Without something listening, the afterware won't be executed.
afterwareStream.listen((_){});
}

// TODO(redbrogdon): Figure out how to guarantee that only one action is in
// the stream at a time. Also figure out if that's really necessary.
get dispatcher => (Action action) => _dispatchController
.add(MiddlewareContext(dispatcher, states.value, action));
.add(WareContext(dispatcher, states.value, action));
}

/// A Business logic component that can apply middleware and reducer
/// functionality to a [Store] by transforming the streams passed into its
/// [applyMiddleware] and [applyReducer] methods.
/// A Business logic component that can apply middleware, reducer, and
/// afterware functionality to a [Store] by transforming the streams passed into
/// its [applyMiddleware], [applyReducer], and [applyAfterware] methods.
abstract class Bloc<S> {
Stream<MiddlewareContext<S>> applyMiddleware(
Stream<MiddlewareContext<S>> input);
Stream<WareContext<S>> applyMiddleware(
Stream<WareContext<S>> input);

Stream<Accumulator<S>> applyReducer(Stream<Accumulator<S>> input);
}

typedef Action MiddlewareFunction<S>(
DispatchFunction dispatcher, S state, Action action);
typedef S ReducerFunction<S>(S state, Action action);
Stream<WareContext<S>> applyAfterware(
Stream<WareContext<S>> input);
}

/// A convenience [Bloc] class that handles the stream mapping bits for you.
/// Subclasses can simply override the [middleware] and [reducer] getters to
/// return their implementations.
/// Subclasses can simply override [middleware], [reducer], and [afterware] to
/// add their implementations.
abstract class SimpleBloc<S> implements Bloc<S> {
@override
Stream<MiddlewareContext<S>> applyMiddleware(
Stream<MiddlewareContext<S>> input) {
Stream<WareContext<S>> applyMiddleware(
Stream<WareContext<S>> input) {
return input.asyncMap((context) async {
return context.copyWith(
await middleware(context.dispatcher, context.state, context.action));
Expand All @@ -162,8 +150,21 @@ abstract class SimpleBloc<S> implements Bloc<S> {
});
}

@override
Stream<WareContext<S>> applyAfterware(
Stream<WareContext<S>> input) {
return input.asyncMap((context) async {
return context.copyWith(
await afterware(context.dispatcher, context.state, context.action));
});
}

FutureOr<Action> middleware(
DispatchFunction dispatcher, S state, Action action) =>
DispatchFunction dispatcher, S state, Action action) =>
action;

FutureOr<Action> afterware(
DispatchFunction dispatcher, S state, Action action) =>
action;

S reducer(S state, Action action) => state;
Expand Down
Loading

0 comments on commit 7118941

Please sign in to comment.