Implementation of a Redux store and the Todo app from Redux's basic tutorial in Kotlin. Uses Kotlin's Flow for publishing updates.
// state
data class Todo(val text: String, val done: Boolean = false)
enum class VisibilityFilter { ALL, DONE, TODO }
data class TodoAppState(
val todos: List<Todo> = emptyList(),
val filter: VisibilityFilter = VisibilityFilter.ALL
)
// actions
sealed class Action {
sealed class TodoAction : Action() {
data class AddTodo(val text: String) : TodoAction()
data class ToggleTodo(val index: Int) : TodoAction()
}
data class ChangeVisibility(val filter: VisibilityFilter) : Action()
}
// reducers
val todoReducer: Reducer<TodoAction, List<Todo>> = { action, todos ->
when (action) {
is AddTodo -> todos + Todo(action.text)
is ToggleTodo -> todos.update(action.index) { it.copy(done = !it.done) }
}
}
val appReducer: Reducer<Action, TodoAppState> = { action, state ->
when (action) {
is TodoAction -> state.copy(todos = todoReducer(action, state.todos))
is ChangeVisibility -> state.copy(filter = action.filter)
}
}
class RxStoreTest {
@Test
fun canGetCurrentStateOfStore() {
val store = createStore(initialState = TodoAppState(), reducer = appReducer)
store.dispatch(AddTodo("1"))
assertThat(store.state().todos).containsExactly(Todo(text = "1", done = false))
store.dispatch(ToggleTodo(0))
assertThat(store.state().todos).containsExactly(Todo(text = "1", done = true))
store.dispatch(ChangeVisibility(VisibilityFilter.DONE))
assertThat(store.state().filter).isEqualTo(VisibilityFilter.DONE)
}
@Test
fun canSubscribeToStoreUpdates() {
runBlocking {
val store = createStore(initialState = TodoAppState(), reducer = appReducer)
val subscription: Flow<TodoAppState> = store.updates()
subscription.test {
assertThat(nextItem()).isEqualTo(TodoAppState())
store.dispatch(AddTodo("1"))
assertThat(nextItem().todos).containsExactly(Todo(text = "1", done = false))
store.dispatch(ToggleTodo(0))
assertThat(nextItem().todos).containsExactly(Todo(text = "1", done = true))
store.dispatch(ChangeVisibility(VisibilityFilter.DONE))
assertThat(nextItem().filter).isEqualTo(VisibilityFilter.DONE)
cancelAndIgnoreRemainingEvents()
}
}
}
@Test
fun canFilterOnSubTree() {
runBlocking {
val store = createStore(initialState = TodoAppState(), reducer = appReducer)
val filterUpdates = store.updates().map { s -> s.filter }.distinctUntilChanged()
filterUpdates.test {
store.dispatch(ChangeVisibility(VisibilityFilter.DONE))
expectNextItemEquals(VisibilityFilter.DONE)
store.dispatch(AddTodo("1"))
store.dispatch(ChangeVisibility(VisibilityFilter.DONE))
expectNoEvents()
store.dispatch(ChangeVisibility(VisibilityFilter.ALL))
expectNextItemEquals(VisibilityFilter.ALL)
cancelAndIgnoreRemainingEvents()
}
}
}
@Test
fun throwingReducerDoesNotBreakStoreAndDoesNotEmitUpdate() {
runBlocking {
val store = createStore(10, { a: Int, s: Int -> s / a })
val updates = store.updates()
updates.test {
expectNextItemEquals(10)
store.dispatch(0) // division by zero
expectNoEvents()
store.dispatch(10)
assertThat(nextItem()).isEqualTo(1)
cancelAndIgnoreRemainingEvents()
}
}
}
// ... more tests and examples
}
Uses Vavr for immutable collections.
class JavaStoreTest {
// state
static class Todo {
final String text;
final boolean done;
Todo(String text, boolean done) {
this.text = text;
this.done = done;
}
}
enum VisibilityFilter {ALL, DONE, TODO}
static class Todos {
final List<Todo> todos;
final VisibilityFilter filter;
Todos(List<Todo> todos, VisibilityFilter filter) {
this.todos = todos;
this.filter = filter;
}
public static Todos initial() {
return new Todos(List.empty(), VisibilityFilter.ALL);
}
}
// actions
interface Action {
}
static class AddTodo implements Action {
final String text;
AddTodo(String text) {
this.text = text;
}
}
static class ToggleTodo implements Action {
final int index;
ToggleTodo(int index) {
this.index = index;
}
}
static class ChangeVisibility implements Action {
final VisibilityFilter filter;
ChangeVisibility(VisibilityFilter filter) {
this.filter = filter;
}
}
// reducers
Reducer<Action, Todos> addTodoReducer = reduceOn(AddTodo.class,
(action, state) -> new Todos(state.todos.append(new Todo(action.text, false)), state.filter)
);
Reducer<Action, Todos> toggleReducer = reduceOn(ToggleTodo.class,
(action, state) -> new Todos(state.todos.update(action.index, t -> new Todo(t.text, !t.done)), state.filter)
);
Reducer<Action, Todos> changeVisibilityReducer = reduceOn(ChangeVisibility.class,
(action, state) -> new Todos(state.todos, action.filter)
);
Reducer<Action, Todos> reducer = combine(addTodoReducer, toggleReducer, changeVisibilityReducer);
@Test
void demo() {
JavaStore<Action, Todos> store = createStore(Todos.initial(), reducer);
store.updates.subscribe(s -> System.out.println("State changed " + s));
store.dispatch(new AddTodo("1"));
await().untilAsserted(() -> assertThat(store.state().todos).extracting(t -> t.text).containsExactly("1"));
store.dispatch(new ToggleTodo(0));
await().untilAsserted(() -> assertThat(store.state().todos).extracting(t -> t.done).containsExactly(true));
store.dispatch(new ChangeVisibility(VisibilityFilter.DONE));
await().untilAsserted(() -> assertThat(store.state().filter).isEqualTo(VisibilityFilter.DONE));
}
}