Redux ported to java/android
I've seen a few of these floating around, but this one has some specific benefits over other implementations.
repositories {
mavenCentral()
}
dependencies {
compile "me.tatarka.redux:redux-core:0.11"
compile "me.tatarka.redux:redux-android:0.11"
compile "me.tatarka.redux:redux-android-lifecycle:0.11"
compile "me.tatarka.redux:redux-thunk:0.11"
compile "me.tatarka.redux:redux-rx:0.11"
compile "me.tatarka.redux:redux-rx2:0.11"
}
Create a store.
SimpleStore<State> store = new SimpleStore(initialState);
Get the current state.
State state = store.state();
Listen to state changes.
store.addListener(new Listener<State>() {
@Override
public void onNewState(State state) {
...
}
});
Or with rxjava (using redux-rx).
ObservableAdapter.observable(store).subscribe(state -> { ... });
Or with rxjava2 (using redux-rx2).
FlowableAdapter.flowable(store).subscribe(state -> { ... });
Create a dispatcher with optional middleware.
Dispatcher<Action, Action> dispatcher = Dispatcher.forStore(store, reducer)
.chain(middleware...);
Dispatch actions.
dispatcher.dispatch(new MyAction());
You can observe your store with LiveData
which will properly tie into the android lifecycle.
LiveDataAdapter.liveData(store).observe(this, state -> { ... });
You can use StoreViewModel
to keep your store around for the lifetime of an activity/fragment
surviving configuration changes.
public class MyViewModel extends StoreViewModel<State, MyViewModel> {
public MyViewModel() {
super(new MyStore());
}
}
public class MyActivity extends LifecycleActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
MyStore store = viewModel.getStore();
viewModel.getState().observe(this, state -> { ... });
}
}
Since LiveData
relays state changes to the main thread, you may lose important stack trace info.
You can get it back by calling LiveDataAdapter.liveData(store, true)
or
LiveDataAdapter.setDebugAll(true)
. This creates an expensive stacktrace on every dispatch so you
probably don't want it on release. A common pattern would be to put
LiveDataAdapter.setDebugAll(BuildConfig.DEBUG)
in your setup code.
It's common you'd want to switch on actions values or class type. Reducers.matchValue()
and
Reducers.matchClass()
makes this easy.
Reducer<String, State> reducer = Reducers.matchValue()
.when("action1", new Action1Reducer())
.when("action2", new Action2Reducer());
Reducer<Object, State> reducer = Reducers.matchClass()
.when(Action1.class, new Action1Reducer())
.when(Action2.class, new Action2Reducer());
There is also Reducers.match()
which takes a predicate for more complicated matching setups.
Reducer<Object, State> reducer = Reducers.match()
.when(Predicates.is("action1"), new Action1Reducer())
.when(Predicates.instanceOf(Action2.class), new Action2Reducer());
You can also run a sequence of reducers with Reducers.all(reducer1, reducer2, ...)
or run reducers
until one changes the state with Reducers.first(reducer1, reducer2, ...)
.
Allows you to dispatch async functions as actions.
SimpleStore<State> store = new SimpleStore<>(initialState);
ThunkDispatcher<Action, Action> dispatcher = new ThunkDispatcher<>(Dispatcher.forStore(store, reducer));
dispatcher.dispatch(new Thunk<Action, Action>() {
@Override
public void run(Dispatcher<Action, Action> dispatcher) {
dispatcher.dispatch(new StartLoading());
someAsyncCall(new Runnable() {
@Override
public void run() {
dispatcher.dispatch(new StopLoading());
}
}
}
});
Alternatively, you can use the ObservableDispatcher
to dispatch a stream of actions.
SimpleStore<State> store = new SimpleStore<>(initialState);
ObservableDispatcher<Action> dispatcher = new ObservableDispatcher<>(Dispatcher.forStore(store, reducer));
dispatcher.dispatch(callThatReturnsObservable()
.map(result -> new StopLoading())
.startWith(Observable.just(new StartLoading())));
Don't want to have to worry about passing around the store and dispatchers? You can subclass
SimpleStore
and create your own dispatch methods. This also simplifies generics a bit when using
throughout your app.
public class MyStore extends SimpleStore<State> {
private final Dispatcher<Action, Action> dispatcher;
private final ObservableDispatcher<Action> observableDispatcher;
public MyStore() {
super(new State());
dispatcher = Dispatcher.forStore(this, new MyReducer())
.chain(new LogMiddleware<>("ACTION"));
observableDispatcher = new ObservableDispatcher<>(dispatcher);
}
public Action dispatch(Action action) {
return dispatcher.dispatch(action);
}
public Subscription dispatch(Observable<Action> actions) {
return observableDispatcher.dispatch(actions);
}
public Observable<Action> observable() {
return ObservableAdapter.observable(this);
}
}
Now you can just pass the single store around and call store.dispatch()
.
You can log all actions on android with the built-in LogMiddleware
.
dispatcher = Dispatcher.forStore(store, reducer)
.chain(new LogMiddleware<Action, Action>("ACTION"));
You can disable/enable actions and see how that effects your ui with the replay middleware. It will replay your modified actions back on the initial state.
compile "me.tatarka.redux:redux-replay:0.10"
replay = new ReplayMiddleware<State, Action, Action>(store, reducer);
dispatcher = Dispatcher.forStore(store, reducer)
.chain(replay);
replay.actions() // lists all actions that have been dispatched
replay.disable(index) // disables action at given index
replay.enable(index) // enables action at the given index
The sample android app includes a debug drawer to let you interact with this middleware.
You can connect to RemoteDev Server to interact with various redux debugging UI's. Currently only displaying actions/state is supported.
npm install -g remotedev-server
remotedev --hostname=localhost --port=8000
compile "me.tatarka.redux:redux-monitor:0.11"
dispatcher = Dispatcher.forStore(store, reducer)
.chain(new MonitorMiddleware(store));