Redux Compact

Redux Compact is a library that aims to reduce the massive amount of boilerplate that follows maintaining a Flutter Redux application.

The library provides a set of middleware and a reducer that intercepts a special kind of action called Compact Action. The action has direct access to the state and can be both sync- and asynchronous and provides you the ability to chain actions. The action’s reducer is a method called reduce which allows you to make corresponding state changes within the action itself.

This approach eliminates the need to maintain separate files and folders for actions and reducers and to create and dispatch multiple actions to handle asynchronous state.

Usage

This documentation assumes that you are already familiar with Redux and Flutter Redux, their core concepts, setup and usage.

For a full overview visit the examples

Create your store as recommended by Flutter Redux. Add Redux Compact middleware and reducer to the store.

final compactReducer = ReduxCompact.createReducer<AppState>();
final compactMiddleware = ReduxCompact.createMiddleware<AppState>();

final store = new Store<AppState>(
  compactReducer, // <-- Add compactReducer
  initialState: AppState.initialState(),
  middleware: [
    compactMiddleware, // <-- Add compactMiddleware
  ],
);

Then dispatch an action that extends CompactAction

class IncrementCountAction extends CompactAction<AppState> {
  final int incrementBy;

  IncrementCountAction(this.incrementBy);

  @override
  AppState reduce() {
    return state.copy(
      counter: state.counter + incrementBy
    );
  }
}

Global error handling

You can add a global error handler to all your asynchrounus actions simply by implementing the onError function when creating the middleware. The function accepts dynamic error and the dispatch function as parameters.

  ReduxCompact.createMiddleware<AppState>(
    onError: (error, dispatch) => yourErrorFunction(error, dispatch)
  );

Compact Action

In order to use Redux Compact you must dispatch an action that extends a CompactAction.

All compact actions must implement the reduce method which is the reducer for the action. The reduce method has direct access to instance variables, dispatch function and the store state.

Keep in mind

  • Like normal reducers the reduce method always expects to return a state. If you do not wish to update the state, simply return state (returning null updates the state to null).
  • Even though the reduce method has access to the dispatch function, dispatching an action in a reducer is an anti-pattern. If you wish to dispatch another action read the chaining actions section.

Sync action

class IncrementCountAction extends CompactAction<AppState> {
  final int incrementBy;

  IncrementCountAction(this.incrementBy);

  @override
  AppState reduce() {
    return state.copy(
      counter: state.counter + incrementBy,
    );
  }
}

Async Action

To create an asynchronous action you simply need to implement the makeRequest() method as a Future. As your request executes the values of request instance variable change allowing you to make state changes accordingly.

The request object contains:

  • loading: true if a request is in flight, false otherwise
  • error: dynamic if an error occurs, null otherwise
  • data: dynamic if the request is successful, null otherwise
class IncrementCountAction extends CompactAction<AppState> {
  final int incrementBy;

  IncrementCountAction(this.incrementBy);

  @override
  makeRequest() {
    // The makeRequest() method has direct access to
    // instance variable and current state
    const count = state.counter + incrementBy;
    final url = "http://numbersapi.com/$count";

    // Return a future
    return http.read(url);
  }

  @override
  AppState reduce() {
    // If we are waiting for a response loading, will be true.
    if (request.loading) {
      return state.copy(isLoading: request.loading);
    }

    // hasError is a helper function that checks if error != null
    if (request.hasError) {
      return state.copy(
        errorMsg: error.message,
      );
    }

    // The request was successful
    return state.copy(
      counter: state.counter + 1,
      description: request.data,
    );
  }
}

Chaining Actions

Compact Action provides two helper functions: before and after. Both of these methods have direct access to the state, dispatch function and class instance variables. You can therefore call or dispatch, other functions or actions, before or after the current action runs.

  • The before method always runs before the reduce method and asynchronous requests.

  • The after method runs after the reduce method, or when an asynchronous request has finished successfully. If an error occurred in an asynchronous request the method will not run.

Just remember the state:

  • Has not changed in the before method
  • Has changed in the after method
class IncrementCountAction extends CompactAction<AppState> {
  final int incrementBy;

  IncrementCountAction(this.incrementBy);

  @override
  AppState reduce(RequestStatus status) {
    // The reducer only increments the count
    return state.copy(
      counter: state.counter + incrementBy,
    );
  }

  @override
  void before() {
    // Before the reducer runs we dispatch an action
    // that copies the current state.
    dispatch(SetPreviousDescAction());
  }

  @override
  void after() {
    // After the reduce method runs
    // We dispatch an action to fetch a description for the counter
    dispatch(FetchDescriptionAction(state.counter));
  }
}

BaseModel

BaseModel is a convenient class to quickly create a ViewModel for the Redux StoreConnector. It has direct access to the store state and the dispatch function. You can therefore dispatch an action within the model or the widget.

BaseModel is not a mandatory class for Redux Compact. You can use basic ViewModels if you want as long as you dispatch a CompactAction. If you would like to use a BaseModel, create a class that extends BaseModel and implement the fromStore method.

class _VM extends BaseModel<AppState> {
  final int count;

  _VM(Store store, { this.count }) : super(store);

  @override
  AppState fromStore() {
    return _VM(store, count: state.counter);
  }

  // You can dispatch an action from the VM
  // or call vm.dispatch(...) from the widget
  void incrementCount() {
    dispatch(IncrementCountAction(state.counter + 1));
  }
}

Then initialize BaseModel with the fromStore method in the in the Widget's StoreConnector

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _VM>(
      converter: (store) => _VM(store).fromStore(), // <-- Initialize the VM
      builder: (context, vm) => Container(...),
    );
  }

Libraries

redux_compact