Giter VIP home page Giter VIP logo

droidux's Introduction

Droidux

Build Status Download Apache 2.0 Android Arsenal

Droidux is "predictable state container" implementation, inspired by Redux.

Features

Droidux is influenced by Three principles of Redux.

  • Single source of truth
    • The state of your whole application is stored in an object tree inside a single store.
  • State is read-only
    • The only way to mutate the state is to emit an action, an object describing what happened.
  • Mutations are written as pure functions
    • To specify how the state tree is transformed by actions, you write pure reducers.

Three Principles | Redux

Features of Droidux are following:

  • All mutations can be observed via Flowable from RxJava
  • All mutations are automatically notified to views via Data Binding

Data flow

Droidux data flow

see also: Introduction to Redux // Speaker Deck (in Japanese)

Installation

Droidux depends on RxJava and Data Binding. Add to your project build.gradle file:

apply plugin: 'com.android.application'

dependencies {
  compile 'info.izumin.android:droidux:0.6.0'
  compile 'io.reactivex.rxjava2:rxjava:2.1.8'
  compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
  annotationProcessor 'info.izumin.android:droidux-processor:0.6.0'
}

And also you need to setup Data Binding.

When you use AsyncAction, you need to add droidux-thunk.

compile 'info.izumin.android:droidux-thunk:0.3.0'

Usage

Quick example

/**
 * This is a state class.
 * It can be as simple as possible implementation, like POJO, or immutable object. 
 */
public class Counter {
    private final int count;

    public Counter(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }
}

/**
 * This is a reducer class.
 * It should be applied @Reducer annotation is given a state class as an argument.
 * It describe whether the reducer should handle which actions.
 */
@Reducer(Counter.class)
public class CounterReducer {

    /**
     * This is a method to handle actions.
     * It should be applied @Dispatchable annotation is given an action class as an parameter.
     * It describe how to transform the state into the next state when dispatched actions.
     * It should return the next state instance, and it is preferred instantiate the new state.
     *
     * This example handle IncrementCountAction,
     + and it returns new counter instance that state is incremented.
     */
    @Dispatchable(IncrementCountAction.class)
    public Counter increment(Counter state) {
        return new Counter(state.getCount() + 1);
    }

    @Dispatchable(DecrementCountAction.class)
    public Counter decrement(Counter state) {
        return new Counter(state.getCount() - 1);
    }

    @Dispatchable(ClearCountAction.class)
    public Counter clear() {
        return new Counter(0);
    }
}


/**
 * This is a store interface.
 * It should be applied @Store annotation and passing reducer classes as parameters.
 * Droidux generates an implementation of getter method, observe method and dispatch method from user-defined interface.
 */
@Store(CounterReducer.class)
public interface CounterStore extends BaseStore {
    Counter getCounter();
    Flowable<Counter> observeCounter();
}

/**
 * They are action classes. They should extend Action class.
 */
public class IncrementCountAction implements Action {}
public class DecrementCountAction implements Action {}
public class ClearCountAction implements Action {}


// Instantiate a Droidux store holding the state of your app.
// Its class is generated automatically from Reducer class.
// 
// The instantiating should use Builder class,
// and it should register a reducer instance and an initial state.
// 
// Its APIs in this example are following:
// - Flowable<Action> dispatch(Action action)
// - Flowable<Counter> observeCounter()
// - Counter getCounter()
CounterStore store = DroiduxCounterStore.builder()
        .setReducer(new CounterReducer(), new Counter(0))
        .build();                                       // Counter: 0

// You can observe to the updates using RxJava interface. 
store.observe((counter) -> Log.d(TAG, counter.toString()));

// The only way to mutate the internal state is to dispatch an action.
store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 1
store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 2
store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 3

store.dispatch(new DecrementCountAction()).subscribe(); // Counter: 2

store.dispatch(new ClearCountAction()).subscribe();     // Counter: 0

Data Binding

// If you use databinding, yor store interface must extend `android.databinding.Observable`.
@Store(CounterReducer.class)
public interface CounterStore extends BaseStore, android.databinding.Observable {
    // You should annotate the getter method with @Bindable
    @Bindable Counter getCounter();
}

CounterStore store = DroiduxCounterStore.builder()
        // Pass the field id generated by DataBinding annotation processor.
        .setReducer(new CounterReducer(), new Counter(0), BR.counter)
        .build();

Layout file is following:

<layout>
    <data>
        <variable android:name="store" android:type="CounterStore" />
    </data>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="@{store.counter}" />
        
    </RelativeLayout>
</layout>

Combined store

@Store({CounterReducer.class, TodoListReducer.class})
class RootStore extends BaseStore {
    Counter getCounter();
    Flowable<Counter> observeCounter();
    TodoList getTodoList();
    Flowable<TodoList> observeTodoList();
}


RootStore store = DroiduxRootStore.builder()
        .setReducer(new CounterReducer(), new Counter(0))
        .setReducer(new TodoListReducer(), new TodoList())
        .addMiddleware(new Logger())
        .build();

store.dispatch(new IncrementCountAction()).subscribe();     // Counter: 1, Todo: 0
store.dispatch(new AddTodoAction("new task")).subscribe();  // Counter: 1, Todo: 1

Middleware

class Logger extends Middleware<CounterStore> {
    @Override
    public Flowable<Action> beforeDispatch(Action action) {
        Log.d("[prev counter]", String.valueOf(getStore().count()));
        Log.d("[action]", action.getClass().getSimpleName());
        return Flowable.just(action);
    }

    @Override
    public Flowable<Action> afterDispatch(Action action) {
        Log.d("[next counter]", String.valueOf(getStore().count()));
        return Flowable.just(action);
    }
}

// Instantiate store class 
CounterStore store = DroiduxCounterStore.builder()
        .setReducer(new CounterReducer(), new Counter(0))
        .addMiddleware(new Logger())        // apply logger middleware
        .build();                           // Counter: 0

store.dispatch(new IncrementCountAction()).subscribe();
// logcat:
// [prev counter]: 0
// [action]: IncrementCountAction
// [next counter]: 1

store.dispatch(new IncrementCountAction()).subscribe();
// logcat:
// [prev counter]: 1
// [action]: IncrementCountAction
// [next counter]: 2

store.dispatch(new ClearCountAction()).subscribe();
// logcat:
// [prev counter]: 2
// [action]: ClearCountAction
// [next counter]: 0

Undo / Redo

class TodoList extends ArrayList<TodoList.Todo> implements UndoableState<TodoList> {
    @Override
    public TodoList clone() {
        // ...
    }

    public static Todo {
        // ...
    }
}

@Undoable
@Reducer(TodoList.class)
class TodoListReducer {
    @Dispatchable(AddTodoAction.class)
    public TodoList add(TodoList state, AddTodoAction action) {
        // ...
    }

    @Dispatchable(CompleteTodoAction.class)
    public TodoList complete(TodoList state, CompleteTodoAction action) {
        // ...
    }
}

@Store(TodoListReducer.class)
public interface TodoListStore {
    TodoList todoList();
    Flowable<TodoList> observeTodoList();
}

class AddTodoAction implements Action {
    // ...
}

class CompleteTodoAction implements Action {
    // ...
}


TodoListStore store = DroiduxTodoListStore.builder()
        .setReducer(new TodoListReducer(), new TodoList())
        .build();

store.dispatch(new AddTodoAction("item 1")).subscribe();        // ["item 1"]
store.dispatch(new AddTodoAction("item 2")).subscribe();        // ["item 1", "item 2"]
store.dispatch(new AddTodoAction("item 3")).subscribe();        // ["item 1", "item 2", "item 3"]
store.dispatch(new CompleteTodoAction("item 2")).subscribe();   // ["item 1", "item 3"]
store.dispatch(new AddTodoAction("item 4")).subscribe();        // ["item 1", "item 3", "item 4"]

store.dispatch(new UndoAction(TodoList.class)).subscribe();
// => ["item 1", "item 3"]

store.dispatch(new UndoAction(TodoList.class)).subscribe();
// => ["item 1", "item 2", "item 3"]

store.dispatch(new RedoAction(TodoList.class)).subscribe();
// => ["item 1", "item 3"]

Async action

Use droidux-thunk.

class FetchTodoListAction implements AsyncAction {
    private final TodoListApi client;

    public FetchTodoListAction(TodoListApi client) {
        this.client = client;
    }

    public Flowable<ReceiveTodoListAction> call(Dispatcher dispatcher) {
        return dispatcher.dispatch(new DoingFetchAction())
                .flatMap(_action -> client.fetch())
                .map(todoList -> {
                    this.todoList = todoList;
                    return new ReceiveTodoListAction(todoList);
                });
    }
}

class ReceiveTodoListAction implements Action {
    private final TodoList todoList;

    public ReceiveTodoListAction(TodoList todoList) {
        this.todoList = todoList;
    }

    public TodoList getTodoList() {
        return todoList;
    }
}


TodoListStore store = DroiduxTodoListStore.builder()
        .setReducer(new TodoListReducer(), new TodoList())
        .addMiddleware(new ThunkMiddleware())
        .build();


store.dispatch(new FetchTodoListAction(client)).subscribe();

Examples

License

Copyright 2015 izumin5210

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

droidux's People

Contributors

izumin5210 avatar k-kagurazaka avatar wakwak3125 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

droidux's Issues

Caused by: java.lang.NoSuchMethodError: com.google.common.collect.FluentIterable.join(Lcom/google/common/base/Joiner;)Ljava/lang/String;

Caused by: java.lang.NoSuchMethodError: com.google.common.collect.FluentIterable.join(Lcom/google/common/base/Joiner;)Ljava/lang/String;
       at info.izumin.android.droidux.processor.generator.StoreImplClassGenerator.createCodeBlock(StoreImplClassGenerator.java:95)
       at info.izumin.android.droidux.processor.generator.StoreImplClassGenerator.createMethodSpec(StoreImplClassGenerator.java:65)
       at info.izumin.android.droidux.processor.generator.StoreImplClassGenerator.createTypeSpec(StoreImplClassGenerator.java:46)
       at info.izumin.android.droidux.processor.generator.StoreImplClassGenerator.createJavaFile(StoreImplClassGenerator.java:41)
       at info.izumin.android.droidux.processor.StoreProcessingStep.process(StoreProcessingStep.java:40)
       at com.google.auto.common.BasicAnnotationProcessor.process(BasicAnnotationProcessor.java:318)
       at com.google.auto.common.BasicAnnotationProcessor.process(BasicAnnotationProcessor.java:171)
       at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
       at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
       at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
       at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
       at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
       at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
       at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:856)
       at com.sun.tools.javac.main.Main.compile(Main.java:523)
       ... 34 more

Combined store

@Store(
        reducers = {
                /*...*/
        }
)
public interface BaseStore {
}

@Store(
        reducers = {
                /*...*/
        },
        stores = {
                /*...*/
        }
)
public interface AppStore {
}

Does this works with Kotlin KAPT?

Trying to implement this using Kotlin Annotation Processor. The issue I can across was that the method defined in the Store interface was not implemented by the annotation tool.

Example

@Store(GuardJavaReducer.class)
public interface AppStoreJava extends BaseStore
{
    List<Guard> getGuards();

    Observable<List<Guard>> observeGuards();
}

Generated code

public final class DroiduxAppStoreJava extends BaseObservable implements AppStoreJava 
{
  @Override
  public List<Guard> getGuards() {
  }

  @Override
  public Observable<List<Guard>> observeGuards() {
  }
}

Both implementations of getGuards and observeGuards did not provide return statement. So, does this library works with Kotlin KAPT or I'm missing out something?

ActionCreators and a typo

class FetchTodoListAction implements AsyncAction {
    private final TodoListApi client;

 // wrong constructor
    public ReceiveTodoListAction(TodoListApi client) {
        this.client = client;
    }

......
  1. too lazy to do a PR for this but it's the wrong constructor =)
  2. you are missing an important part of async actions that are provided to you by action creators, which you do not have. Action creators allow you to dispatch actions! but we can use it in the call method instead
class DoingFetchAction implements Action {}

class FetchTodoListAction implements AsyncAction {
    private final TodoListApi client;

    public FetchTodoListAction (TodoListApi client) {
        this.client = client;
    }

 Observable<ReceiveTodoListAction> call(Dispatcher dispatcher) {
        dispatcher.dispatch(new DoingFetchAction()); // notify you are starting a fetch
        return client.fetch()
                .map(todoList -> {
                    this.todoList = todoList;
                    return new ReceiveTodoListAction(todoList);
                });
    }
}

something like that
see http://rackt.org/redux/docs/advanced/AsyncActions.html

just a thought. the important part is that async actions receive a dispatch function or dispatcher so they can do dispatch themselves.

State with generics

Currently, generated store methods(observer/getter) will be empty implementation.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.