Commit d69fba02 authored by davidmarne's avatar davidmarne

built_redux_tweaks - move selector to the app state object to take advantage of memoized getters

parent 5fa7a464
......@@ -35,10 +35,10 @@ As your app grows, you'll want to break reducers and middleware down into smalle
These two libraries are incredibly similar. These are the minor differences:
* Actions
- `built_redux` - Actions are generated for you by `built_redux` based on a definition. They are then attached to the Store upon creation.
- `built_redux` - Actions are generated for you by `built_redux` based on a definition. They are then attached to the Store upon creation. Each action has a unique name and a generic payload type. Each action can have at most one reducer.
- `Redux`, Actions are plain ol' Dart values, Classes or Enums.
* Reducers
- `built_redux` - Reducers are functions that mutate a `StateBuilder` and return it. The `StateBuilder` is then built after all reducers have run. Enforces immutability.
- `built_redux` - Reducers are void functions that mutate a `StateBuilder`. The `StateBuilder` is then built after all reducers have run. Enforces immutability.
- `redux` - Reducers are functions in app state and latest action and return a new app state. Since immutability is not enforced, a user could simply mutate the state object instead of returning an updated copy.
- Both - Testing is easy, and both libraries have utilities for binding Reducers to Actions of a specific type.
* Middleware
......
......@@ -3,16 +3,13 @@ import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/presentation/extra_actions_button.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
class ExtraActionSelector
extends StoreConnector<AppState, AppStateBuilder, AppActions, bool> {
ExtraActionSelector({Key key}) : super(key: key);
@override
bool connect(AppState state) {
return allCompleteSelector(todosSelector(state));
}
bool connect(AppState state) => state.allCompleteSelector;
@override
Widget build(BuildContext context, bool allComplete, AppActions actions) {
......
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
import 'package:built_redux_sample/presentation/todo_list.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
......@@ -27,10 +26,5 @@ class FilteredTodos
}
@override
List<Todo> connect(AppState state) {
return filteredTodosSelector(
todosSelector(state),
activeFilterSelector(state),
);
}
List<Todo> connect(AppState state) => state.filteredTodosSelector;
}
......@@ -2,7 +2,6 @@ library stats;
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
import 'package:built_redux_sample/presentation/stats_counter.dart';
import 'package:built_value/built_value.dart';
import 'package:flutter/src/widgets/framework.dart';
......@@ -27,8 +26,8 @@ class Stats
@override
StatsProps connect(AppState state) {
return new StatsProps((b) => b
..numCompleted = numCompletedSelector(todosSelector(state))
..numActive = numActiveSelector(todosSelector(state)));
..numCompleted = state.numCompletedSelector
..numActive = state.numActiveSelector);
}
@override
......
......@@ -6,6 +6,7 @@ import 'package:built_redux_sample/models/todo.dart';
import 'package:built_redux_sample/models/visibility_filter.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:quiver/core.dart';
part 'app_state.g.dart';
......@@ -33,4 +34,40 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
factory AppState.fromTodos(List<Todo> todos) =>
new AppState((b) => b..todos = new ListBuilder<Todo>(todos));
}
\ No newline at end of file
/// [numCompletedSelector] memoizes and returns the number of complete todos.
@memoized
int get numCompletedSelector =>
todos.fold(0, (sum, todo) => todo.complete ? ++sum : sum);
/// [numActiveSelector] returns and memoizes the number of active todos.
/// Note it is computed using numCompletedSelector. Since `numCompletedSelector` is memoized, this is
/// cheaper than iterating over all todos again by doing todos.fold(0, (sum, todo) => !todo.complete ? ++sum : sum);
@memoized
int get numActiveSelector => todos.length - numCompletedSelector;
/// [allCompleteSelector] returns and memoizes a boolean value which is true if all todos are complete.
/// Note it is computed using numCompletedSelector. Since `numCompletedSelector` is memoized, this is
/// cheaper than iterating over all todos again by doing todos.every((t) => t.completed);
@memoized
bool get allCompleteSelector => numCompletedSelector == 0;
@memoized
List<Todo> get filteredTodosSelector => todos.where((todo) {
if (activeFilter == VisibilityFilter.all) {
return true;
} else if (activeFilter == VisibilityFilter.active) {
return !todo.complete;
} else if (activeFilter == VisibilityFilter.completed) {
return todo.complete;
}
}).toList();
Optional<Todo> todoSelector(String id) {
try {
return new Optional.of(todos.firstWhere((todo) => todo.id == id));
} catch (e) {
return new Optional.absent();
}
}
}
import 'package:built_collection/built_collection.dart';
import 'package:built_redux/built_redux.dart';
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
var reducerBuilder = new ReducerBuilder<AppState, AppStateBuilder>()
..add(AppActionsNames.addTodoAction, addTodo)
......@@ -15,52 +13,50 @@ var reducerBuilder = new ReducerBuilder<AppState, AppStateBuilder>()
..add(AppActionsNames.loadTodosSuccess, todosLoaded)
..add(AppActionsNames.loadTodosFailure, todosLoadFailed);
addTodo(AppState state, Action<Todo> action, AppStateBuilder builder) =>
builder.todos = state.todos.toBuilder()..add(action.payload);
addTodo(AppState state, Action<Todo> action, AppStateBuilder builder) {
builder.todos.add(action.payload);
}
clearCompleted(AppState state, Action<Null> action, AppStateBuilder builder) =>
builder.todos = state.todos.toBuilder()..where((todo) => !todo.complete);
clearCompleted(AppState state, Action<Null> action, AppStateBuilder builder) {
builder.todos.where((todo) => !todo.complete);
}
deleteTodo(AppState state, Action<String> action, AppStateBuilder builder) =>
builder.todos = state.todos.toBuilder()
..where((todo) => todo.id != action.payload);
deleteTodo(AppState state, Action<String> action, AppStateBuilder builder) {
builder.todos.where((todo) => todo.id != action.payload);
}
toggleAll(AppState state, Action<Null> action, AppStateBuilder builder) {
final allComplete = allCompleteSelector(todosSelector(state));
final allComplete = state.allCompleteSelector;
return builder.todos = state.todos.toBuilder()
..map((todo) => (todo.toBuilder()..complete = !allComplete).build());
}
updateFilter(
AppState state,
Action<VisibilityFilter> action,
AppStateBuilder builder,
) =>
builder.activeFilter = action.payload;
AppState state, Action<VisibilityFilter> action, AppStateBuilder builder) {
builder.activeFilter = action.payload;
}
updateTab(AppState state, Action<AppTab> action, AppStateBuilder builder) =>
builder.activeTab = action.payload;
updateTab(AppState state, Action<AppTab> action, AppStateBuilder builder) {
builder.activeTab = action.payload;
}
todosLoaded(
AppState state, Action<List<Todo>> action, AppStateBuilder builder) {
builder
..isLoading = false
..todos = new ListBuilder<Todo>(action.payload);
..todos.addAll(action.payload);
}
todosLoadFailed(
AppState state, Action<Object> action, AppStateBuilder builder) {
builder
..isLoading = false
..todos = new ListBuilder<Todo>([]);
..todos.clear();
}
updateTodo(
AppState state,
Action<UpdateTodoActionPayload> action,
AppStateBuilder builder,
) =>
builder.todos = state.todos.toBuilder()
..map((todo) =>
todo.id == action.payload.id ? action.payload.updatedTodo : todo);
updateTodo(AppState state, Action<UpdateTodoActionPayload> action,
AppStateBuilder builder) {
builder.todos.map((todo) =>
todo.id == action.payload.id ? action.payload.updatedTodo : todo);
}
import 'package:built_redux_sample/models/models.dart';
import 'package:quiver/core.dart';
List<Todo> todosSelector(AppState state) => state.todos.toList();
VisibilityFilter activeFilterSelector(AppState state) => state.activeFilter;
AppTab activeTabSelector(AppState state) => state.activeTab;
bool isLoadingSelector(AppState state) => state.isLoading;
bool allCompleteSelector(List<Todo> todos) =>
todos.every((todo) => todo.complete);
int numActiveSelector(List<Todo> todos) =>
todos.fold(0, (sum, todo) => !todo.complete ? ++sum : sum);
int numCompletedSelector(List<Todo> todos) =>
todos.fold(0, (sum, todo) => todo.complete ? ++sum : sum);
List<Todo> filteredTodosSelector(
List<Todo> todos,
VisibilityFilter activeFilter,
) {
return todos.where((todo) {
if (activeFilter == VisibilityFilter.all) {
return true;
} else if (activeFilter == VisibilityFilter.active) {
return !todo.complete;
} else if (activeFilter == VisibilityFilter.completed) {
return todo.complete;
}
}).toList();
}
Optional<Todo> todoSelector(List<Todo> todos, String id) {
try {
return new Optional.of(todos.firstWhere((todo) => todo.id == id));
} catch (e) {
return new Optional.absent();
}
}
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
import 'package:test/test.dart';
main() {
......@@ -15,7 +14,7 @@ main() {
),
]);
expect(numActiveSelector(todosSelector(state)), 2);
expect(state.numActiveSelector, 2);
});
test('should calculate the number of completed todos', () {
......@@ -29,7 +28,7 @@ main() {
),
]);
expect(numCompletedSelector(todosSelector(state)), 1);
expect(state.numActiveSelector, 1);
});
test('should return all todos if the VisibilityFilter is all', () {
......@@ -43,11 +42,8 @@ main() {
),
];
final state = new AppState.fromTodos(todos);
expect(
filteredTodosSelector(todosSelector(state), VisibilityFilter.all),
todos,
);
expect(state.activeFilter, VisibilityFilter.all);
expect(state.filteredTodosSelector, todos);
});
test('should return active todos if the VisibilityFilter is active', () {
......@@ -63,12 +59,10 @@ main() {
todo2,
todo3,
];
final state = new AppState.fromTodos(todos);
final state = new AppState.fromTodos(todos)
.rebuild((b) => b.activeFilter = VisibilityFilter.active);
expect(
filteredTodosSelector(todosSelector(state), VisibilityFilter.active),
[todo1, todo2],
);
expect(state.filteredTodosSelector, [todo1, todo2]);
});
test('should return completed todos if the VisibilityFilter is completed',
......@@ -85,12 +79,10 @@ main() {
todo2,
todo3,
];
final state = new AppState.fromTodos(todos);
final state = new AppState.fromTodos(todos)
.rebuild((b) => b.activeFilter = VisibilityFilter.completed);
expect(
filteredTodosSelector(todosSelector(state), VisibilityFilter.completed),
[todo3],
);
expect(state.filteredTodosSelector, [todo3]);
});
});
}
......@@ -2,7 +2,6 @@ import 'package:built_redux/built_redux.dart';
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/reducers/reducers.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
import 'package:test/test.dart';
main() {
......@@ -87,7 +86,7 @@ main() {
store.actions.toggleAllAction();
expect(allCompleteSelector(todosSelector(store.state)), isTrue);
expect(store.state.allCompleteSelector, isTrue);
});
test('should mark all as incomplete if all todos are complete', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment