-
Notifications
You must be signed in to change notification settings - Fork 11.6k
/
TasksViewModel.java
284 lines (244 loc) · 10 KB
/
TasksViewModel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package com.example.android.architecture.blueprints.todoapp.tasks;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.Pair;
import android.util.Log;
import com.example.android.architecture.blueprints.todoapp.R;
import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskActivity;
import com.example.android.architecture.blueprints.todoapp.data.Task;
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository;
import com.example.android.architecture.blueprints.todoapp.util.schedulers.BaseSchedulerProvider;
import java.util.List;
import rx.Completable;
import rx.Observable;
import rx.subjects.BehaviorSubject;
import rx.subjects.PublishSubject;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* ViewModel for the list of tasks.
*/
public final class TasksViewModel {
@VisibleForTesting
static final String FILTER_KEY = "filter";
private static final String TAG = TasksViewModel.class.getSimpleName();
@NonNull
private final TasksRepository mTasksRepository;
@NonNull
private final TasksNavigator mNavigator;
@NonNull
private final BaseSchedulerProvider mSchedulerProvider;
// using a BehaviourSubject because we are interested in the last object that was emitted before
// subscribing. Like this we ensure that the loading indicator has the correct visibility.
private final BehaviorSubject<Boolean> mLoadingIndicatorSubject;
// using a BehaviourSubject because we are interested in the last object that was emitted before
// subscribing. Like this we ensure that the last selected filter or the default one is used.
@NonNull
private final BehaviorSubject<TasksFilterType> mFilter;
// using a PublishSubject because we are not interested in the last object that was emitted
// before subscribing. Like this we avoid displaying the snackbar multiple times
@NonNull
private final PublishSubject<Integer> mSnackbarText;
public TasksViewModel(@NonNull TasksRepository tasksRepository,
@NonNull TasksNavigator navigationProvider,
@NonNull BaseSchedulerProvider schedulerProvider) {
mTasksRepository = checkNotNull(tasksRepository, "TaskRepository cannot be null");
mNavigator = checkNotNull(navigationProvider, "Navigator cannot be null");
mSchedulerProvider = checkNotNull(schedulerProvider, "SchedulerProvider cannot be null");
mLoadingIndicatorSubject = BehaviorSubject.create(false);
mFilter = BehaviorSubject.create(TasksFilterType.ALL_TASKS);
mSnackbarText = PublishSubject.create();
}
/**
* @return the model for the tasks list.
*/
@NonNull
public Observable<TasksUiModel> getUiModel() {
return getTaskItems()
.doOnSubscribe(() -> mLoadingIndicatorSubject.onNext(true))
.doOnNext(__ -> mLoadingIndicatorSubject.onNext(false))
.doOnError(__ -> mSnackbarText.onNext(R.string.loading_tasks_error))
.switchMap(tasks -> mFilter.map(filterType -> Pair.create(tasks, filterType)))
.map(this::constructTasksModel);
}
@NonNull
private TasksUiModel constructTasksModel(Pair<List<TaskItem>, TasksFilterType> pair) {
List<TaskItem> tasks = pair.first;
TasksFilterType filterType = pair.second;
int filterTextResId = getFilterText(filterType);
boolean isTasksListVisible = !tasks.isEmpty();
boolean isNoTasksViewVisible = !isTasksListVisible;
NoTasksModel noTasksModel = null;
if (tasks.isEmpty()) {
noTasksModel = getNoTasksModel(filterType);
}
return new TasksUiModel(filterTextResId, isTasksListVisible, tasks, isNoTasksViewVisible,
noTasksModel);
}
private Observable<List<TaskItem>> getTaskItems() {
return Observable.combineLatest(mTasksRepository.getTasks(),
mFilter,
Pair::create)
.flatMap(pair -> Observable.from(pair.first)
.filter(task -> shouldFilterTask(task, pair.second))
.map(this::constructTaskItem)
.toList());
}
private NoTasksModel getNoTasksModel(TasksFilterType mCurrentFiltering) {
switch (mCurrentFiltering) {
case ACTIVE_TASKS:
return new NoTasksModel(R.string.no_tasks_active,
R.drawable.ic_check_circle_24dp, false);
case COMPLETED_TASKS:
return new NoTasksModel(R.string.no_tasks_completed,
R.drawable.ic_verified_user_24dp, false);
default:
return new NoTasksModel(R.string.no_tasks_all,
R.drawable.ic_assignment_turned_in_24dp, true);
}
}
@NonNull
private TaskItem constructTaskItem(Task task) {
@DrawableRes int background = task.isCompleted()
? R.drawable.list_completed_touch_feedback
: R.drawable.touch_feedback;
return new TaskItem(task, background,
() -> handleTaskTaped(task),
checked -> handleTaskChecked(task, checked));
}
private void handleTaskTaped(Task task) {
mNavigator.openTaskDetails(task.getId());
}
private void handleTaskChecked(Task task, boolean checked) {
Completable checkTask = checked ? completeTask(task) : activateTask(task);
checkTask.subscribeOn(mSchedulerProvider.computation())
.observeOn(mSchedulerProvider.computation())
.subscribe(
//on Completed
() -> {
},
// on error
throwable -> Log.e(TAG, "Error completing or activating task")
);
}
private Completable completeTask(Task completedTask) {
return mTasksRepository.completeTask(completedTask)
.doOnCompleted(() -> mSnackbarText.onNext(R.string.task_marked_complete));
}
private Completable activateTask(Task activeTask) {
return mTasksRepository.activateTask(activeTask)
.doOnCompleted(() -> mSnackbarText.onNext(R.string.task_marked_active));
}
/**
* Trigger a force update of the tasks.
*/
public Completable forceUpdateTasks() {
mLoadingIndicatorSubject.onNext(true);
return mTasksRepository.refreshTasks()
.doOnTerminate(() -> mLoadingIndicatorSubject.onNext(false));
}
/**
* Open the {@link AddEditTaskActivity}
*/
public void addNewTask() {
mNavigator.addNewTask();
}
/**
* Handle the response received on onActivityResult.
*
* @param requestCode the request with which the Activity was opened.
* @param resultCode the result of the Activity.
*/
public void handleActivityResult(int requestCode, int resultCode) {
// If a task was successfully added, show snackbar
if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode
&& Activity.RESULT_OK == resultCode) {
mSnackbarText.onNext(R.string.successfully_saved_task_message);
}
}
@NonNull
private Boolean shouldFilterTask(Task task, TasksFilterType filter) {
switch (filter) {
case ACTIVE_TASKS:
return task.isActive();
case COMPLETED_TASKS:
return task.isCompleted();
case ALL_TASKS:
default:
return true;
}
}
@StringRes
private int getFilterText(TasksFilterType filter) {
switch (filter) {
case ACTIVE_TASKS:
return R.string.label_active;
case COMPLETED_TASKS:
return R.string.label_completed;
case ALL_TASKS:
default:
return R.string.label_all;
}
}
/**
* Sets the current task filtering type.
*
* @param filter Can be {@link TasksFilterType#ALL_TASKS},
* {@link TasksFilterType#COMPLETED_TASKS}, or
* {@link TasksFilterType#ACTIVE_TASKS}
*/
public void filter(TasksFilterType filter) {
mFilter.onNext(filter);
}
/**
* Restore the state of the view based on a bundle.
*
* @param bundle the bundle containing the state.
*/
public void restoreState(@Nullable Bundle bundle) {
if (bundle != null && bundle.containsKey(FILTER_KEY)) {
TasksFilterType filterType = (TasksFilterType) bundle.getSerializable(FILTER_KEY);
mFilter.onNext(filterType);
}
}
/**
* @return the state of the view that needs to be saved.
*/
@NonNull
public Bundle getStateToSave() {
Bundle bundle = new Bundle();
bundle.putSerializable(FILTER_KEY, mFilter.getValue());
return bundle;
}
/**
* Clear the list of completed tasks and refresh the list.
*
* @return a Completable that emits when the tasks are cleared or error.
*/
@NonNull
public Completable clearCompletedTasks() {
return Completable.fromAction(this::clearCompletedTasksAndNotify);
}
private void clearCompletedTasksAndNotify() {
mTasksRepository.clearCompletedTasks();
mSnackbarText.onNext(R.string.completed_tasks_cleared);
}
/**
* @return a stream of string ids that should be displayed in the snackbar.
*/
@NonNull
public Observable<Integer> getSnackbarMessage() {
return mSnackbarText.asObservable();
}
/**
* @return a stream that emits true if the progress indicator should be displayed, false otherwise.
*/
@NonNull
public Observable<Boolean> getLoadingIndicatorVisibility() {
return mLoadingIndicatorSubject.asObservable();
}
}