• Stars
    star
    141
  • Rank 259,971 (Top 6 %)
  • Language
    Dart
  • License
    MIT License
  • Created about 8 years ago
  • Updated almost 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Redux.dart middleware for handling actions using Dart Streams

Redux Epics

Travis Build Status

Redux is great for synchronous updates to a store in response to actions. However, working with complex asynchronous operations, such as autocomplete search experiences, can be a bit tricky with traditional middleware. This is where Epics come in!

The best part: Epics are based on Dart Streams. This makes routine tasks easy, and complex tasks such as asynchronous error handling, cancellation, and debouncing a breeze.

Note: For users unfamiliar with Streams, simple async cases are easier to handle with a normal Middleware Function. If normal Middleware Functions or Thunks work for you, you're doing it right! When you find yourself dealing with more complex scenarios, such as writing an Autocomplete UI, check out the Recipes below to see how Streams / Epics can make your life easier.

Example

Let's say your app has a search box. When a user submits a search term, you dispatch a PerformSearchAction which contains the term. In order to actually listen for the PerformSearchAction and make a network request for the results, we can create an Epic!

In this instance, our Epic will need to filter all incoming actions it receives to only the Action it is interested in: the PerformSearchAction. This will be done using the where method on Streams. Then, we need to make a network request with the search term using asyncMap method. Finally, we need to transform those results into an action that contains the search results. If an error has occurred, we'll want to return an error action so our app can respond accordingly.

Here's what the above description looks like in code.

import 'dart:async';
import 'package:redux_epics/redux_epics.dart';

Stream<dynamic> exampleEpic(Stream<dynamic> actions, EpicStore<State> store) {
  return actions
    .where((action) => action is PerformSearchAction)
    .asyncMap((action) => 
      // Pseudo api that returns a Future of SearchResults
      api.search((action as PerformSearch).searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}

Connecting the Epic to the Redux Store

Now that we've got an epic to work with, we need to wire it up to our Redux store so it can receive a stream of actions. In order to do this, we'll employ the EpicMiddleware.

import 'package:redux_epics/redux_epics.dart';
import 'package:redux/redux.dart';

var epicMiddleware = new EpicMiddleware(exampleEpic);
var store = new Store<State>(fakeReducer, middleware: [epicMiddleware]);

Combining epics and normal middleware

To combine the epic Middleware and normal middleware, simply use both in the list! Note: You may need to provide

var store = new Store<AppState>(
  fakeReducer,
  middleware: [myMiddleware, EpicMiddleware<AppState>(exampleEpic)],
);

If you're combining two Lists, please make sure to use the + or the ... spread operator.

var store = new Store<AppState>(
  fakeReducer,
  middleware: [myMiddleware] + [EpicMiddleware<AppState>(exampleEpic)],
);

Combining Epics

Rather than having one massive Epic that handles every possible type of action, it's best to break Epics down into smaller, more manageable and testable units. This way we could have a searchEpic, a chatEpic, and an updateProfileEpic, for example.

However, the EpicMiddleware accepts only one Epic. So what are we to do? Fear not: redux_epics includes class for combining Epics together!

import 'package:redux_epics/redux_epics.dart';
final epic = combineEpics<State>([
  searchEpic, 
  chatEpic, 
  updateProfileEpic,
]);

Advanced Recipes

In order to perform more advanced operations, it's often helpful to use a library such as RxDart.

Casting

In order to use this library effectively, you generally need filter down to actions of a certain type, such as PerformSearchAction. In the previous examples, you'll noticed that we need to filter using the where method on the Stream, and then manually cast (action as SomeType) later on.

To more conveniently narrow down actions to those of a certain type, you have two options:

TypedEpic

The first option is to use the built-in TypedEpic class. This will allow you to write Epic functions that handle actions of a specific type, rather than all actions!

final epic = new TypedEpic<State, PerformSearchAction>(searchEpic);

Stream<dynamic> searchEpic(
  // Note: This epic only handles PerformSearchActions
  Stream<PerformSearchAction> actions, 
  EpicStore<State> store,
) {
  return actions
    .asyncMap((action) =>
      // No need to cast the action to extract the search term!
      api.search(action.searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}

RxDart

You can use the whereType method provided by RxDart. It will both perform a where check and then cast the action for you.

import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';

Stream<dynamic> ofTypeEpic(Stream<dynamic> actions, EpicStore<State> store) {
  // Wrap our actions Stream as an Observable. This will enhance the stream with
  // a bit of extra functionality.
  return actions
    // Use `whereType` to narrow down to PerformSearchAction 
    .whereType<PerformSearchAction>()
    .asyncMap((action) =>
      // No need to cast the action to extract the search term!
      api.search(action.searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}

Cancellation

In certain cases, you may need to cancel an asynchronous task. For example, your app begins loading data in response to a user clicking on a the search button by dispatching a PerformSearchAction, and then the user hit's the back button in order to correct the search term. In that case, your app dispatches a CancelSearchAction. We want our Epic to cancel the previous search in response to the action. So how can we accomplish this?

This is where Observables really shine. In the following example, we'll employ Observables from the RxDart library to beef up the power of streams a bit, using the switchMap and takeUntil operator.

import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';

Stream<dynamic> cancelableSearchEpic(
  Stream<dynamic> actions,
  EpicStore<State> store,
) {
  return actions
      .whereType<PerformSearchAction>()
      // Use SwitchMap. This will ensure if a new PerformSearchAction
      // is dispatched, the previous searchResults will be automatically 
      // discarded.
      //
      // This prevents your app from showing stale results.
      .switchMap((action) {
        return Stream.fromFuture(api.search(action.searchTerm)
            .then((results) => SearchResultsAction(results))
            .catchError((error) => SearchErrorAction(error)))
            // Use takeUntil. This will cancel the search in response to our
            // app dispatching a `CancelSearchAction`.
            .takeUntil(actions.whereType<CancelSearchAction>());
  });
}

Autocomplete using debounce

Let's take this one step further! Say we want to turn our previous example into an Autocomplete Epic. In this case, every time the user types a letter into the Text Input, we want to fetch and show the search results. Each time the user types a letter, we'll dispatch a PerformSearchAction.

In order to prevent making too many API calls, which can cause unnecessary load on your backend servers, we don't want to make an API call on every single PerformSearchAction. Instead, we'll wait until the user pauses typing for a short time before calling the backend API.

We'll achieve this using the debounce operator from RxDart.

import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';

Stream<dynamic> autocompleteEpic(
  Stream<dynamic> actions,
  EpicStore<State> store,
) {
  return actions
      .whereType<PerformSearchAction>()
      // Using debounce will ensure we wait for the user to pause for 
      // 150 milliseconds before making the API call
      .debounce(new Duration(milliseconds: 150))
      .switchMap((action) {
        return Stream.fromFuture(api.search(action.searchTerm)
                .then((results) => SearchResultsAction(results))
                .catchError((error) => SearchErrorAction(error)))
            .takeUntil(actions.whereType<CancelSearchAction>());
  });
}

Dependency Injection

Dependencies can be injected manually with either a Functional or an Object-Oriented style. If you choose, you may use a Dependency Injection or Service locator library as well.

Functional

// epic_file.dart
Epic<AppState> createEpic(WebService service) {
  return (Stream<dynamic> actions, EpicStore<AppState> store) async* {
    service.doSomething()...
  }
}

OO

// epic_file.dart
class MyEpic implements EpicClass<State> {
  final WebService service;

  MyEpic(this.service);

  @override
  Stream<dynamic> call(Stream<dynamic> actions, EpicStore<State> store) {
    service.doSomething()...
  } 
}

Usage - Production

In production code the epics can be created at the point where combineEpics is called. If you're using separate main_<environment>.dart files to configure your application for different environments you may want to pass the config to the RealWebService at this point.

// app_store.dart
import 'package:epic_file.dart';
...

final apiBaseUrl = config.apiBaseUrl

final functionalEpic = createEpic(new RealWebService(apiBaseUrl));
// or
final ooEpic = new MyEpic(new RealWebService(apiBaseUrl));

static final epics = combineEpics<AppState>([
    functionalEpic,
    ooEpic,    
    ...
    ]);
static final epicMiddleware = new EpicMiddleware(epics);

Usage - Testing

...
final testFunctionalEpic = createEpic(new MockWebService());
// or
final testOOEpic = new MyEpic(new MockWebService());
...

More Repositories

1

flutter_architecture_samples

TodoMVC for Flutter
Dart
8,720
star
2

flutter_redux

A library that connects Widgets to a Redux Store
Dart
1,648
star
3

scoped_model

A Widget that passes a Reactive Model to all of it's children
Dart
774
star
4

bansa

A state container for Java & Kotlin, inspired by Redux & Elm
Kotlin
442
star
5

flutter_stetho

Integrate Flutter with the Facebook Stetho tool for Android
Dart
219
star
6

new_flutter_template

Test ideas for a new flutter template
Dart
140
star
7

transparent_image

A transparent image in Dart code, represented as a Uint8List.
Dart
103
star
8

redux_thunk

Redux Middleware for handling functions as actions
Dart
88
star
9

github_search_angular_flutter

Demonstrates how to use the bloc pattern for both an Angular and Flutter app
CSS
85
star
10

flutter_stream_friends

Flutter's great. Streams are great. Let's be friends.
Dart
62
star
11

gradient_animations

An example of how to animate gradients in flutter
Dart
61
star
12

flutter_redux_dev_tools

A Time Traveling Redux Debugger for Flutter
Makefile
40
star
13

reselect_dart

Efficiently derive data from your Redux Store
Dart
37
star
14

redux_logging

Redux.dart Middleware that prints the latest action & state
Dart
30
star
15

rainbow_gradient

An easy way to add Rainbows to your Flutter apps!
Dart
23
star
16

giphy_client

A Giphy API Client for Dart compatible with all platforms
Dart
22
star
17

mvi_sealed_unions

dart_sealed_union playground, started with code from Flutter Consortium
Dart
21
star
18

memory_image_converter

A command line app that will convert an image into a Uint8List which can be consumed by a Flutter app using the `MemoryImage`, `Image.memory`, or `FadeInImage.memoryNetwork` classes.
Dart
16
star
19

hacker_news_client

A client for the Hacker News API. Works on Flutter, Server, and Web.
Dart
16
star
20

hnpwa_client

Fetch data from the HNPWA API -- a Hacker News API crafted for mobile and progressive web apps
Dart
14
star
21

open_iconic_flutter

The Open Iconic icon pack available as a set of Flutter Icons
Dart
14
star
22

rxdart_codemod

A collection of codemods to upgrade your RxDart code from one version to the next
Dart
11
star
23

redux_future

A Redux Middleware for handling Dart Futures as actions
Dart
10
star
24

draggable_scrollbar

A scrollbar that can be used to quickly navigate through a list
Dart
10
star
25

fancy_network_image

A port of cached_network_image all the goodies EXCEPT the cache
Dart
10
star
26

scoped_model_github_search

Translated the RxDart Github Search example to ScopedModel
Dart
9
star
27

redux_dev_tools

Time Travel Dev Tools for Dart Redux
Dart
8
star
28

CSScaffold.tmbundle

Like or want to learn CSScaffold, but tired of referring to the documentation or looking through Mixins.css files? Use the the Textmate bundle!
6
star
29

flutter_lcov_docker

Flutter + Lcov for code Coverage reports!
5
star
30

swapi_client

A library that makes it easy to fetch data from SWAPI: The Star Wars API.
Dart
5
star
31

workshop_live_coding_login

Dart
4
star
32

nested_bottom_navigation

A test of how to do nested bottom navigation with BottomNavigation
Dart
4
star
33

flutter_ui_challenge_zoom_menu

Flutter UI Challenge: Zoom Menu.
Dart
3
star
34

dotfiles

The dotfiles of Brian Egan
HTML
3
star
35

stream_store

A Redux-style Store implemented using core Dart Stream primitives
Dart
3
star
36

hacker_news_app

Hacker news app for flutter
Dart
3
star
37

ez_listenable

A set utilities that allow you to listen to objects and notify listeners of changes
CSS
3
star
38

dribbble_client

A Dribbble api client for Dart
Dart
3
star
39

monocle

View CONTENTdm images with style!
JavaScript
2
star
40

Scaffold.ruble

Aptana Bundle for the Scaffold CSS preprocessor!
Ruby
2
star
41

Playduino

JavaScript
2
star
42

Vim-Configuration

My personal vim folder!
Vim Script
1
star
43

BangumiList

萌豚 η•ͺ剧清单
Dart
1
star
44

loadTest.js

A simple load test script for phantomjs
1
star
45

dmsuggest

A Search Suggestion App for dmBridge
JavaScript
1
star
46

HotTest

The hottest way to run client-side unit tests
JavaScript
1
star
47

pic_in_a_box

A swank replica of Sexy Slider.
JavaScript
1
star
48

Dilefont

Such a dilettante
JavaScript
1
star
49

transmit-vim

Integrates Transmit FTP (Mac) with Vim
Vim Script
1
star
50

dmcarousel

A slideshow application for dmBridge
JavaScript
1
star