diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.kt index d56a67a0a..bb0474126 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.kt @@ -54,10 +54,10 @@ class AddEditTaskFragment : Fragment(), MviView { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt index 50dfe39f4..636ca8a89 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt @@ -33,6 +33,7 @@ import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState import com.example.android.architecture.blueprints.todoapp.util.notOfType import io.reactivex.Observable import io.reactivex.ObservableTransformer +import io.reactivex.Observer import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject @@ -68,9 +69,7 @@ class AddEditTaskViewModel( } } - override fun processIntents(intents: Observable) { - intents.subscribe(intentsSubject) - } + override val intentsObserver: Observer get() = intentsSubject override fun states(): Observable = statesObservable diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviViewModel.kt index 611afb47c..aed6e2a5c 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviViewModel.kt @@ -1,6 +1,7 @@ package com.example.android.architecture.blueprints.todoapp.mvibase import io.reactivex.Observable +import io.reactivex.Observer /** * Object that will subscribes to a [MviView]'s [MviIntent]s, @@ -11,7 +12,7 @@ import io.reactivex.Observable * @param S Top class of the [MviViewState] the [MviViewModel] will be emitting. */ interface MviViewModel { - fun processIntents(intents: Observable) + val intentsObserver: Observer fun states(): Observable } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsFragment.kt index 5f7714145..1be3a8ba5 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsFragment.kt @@ -54,9 +54,9 @@ class StatisticsFragment : Fragment(), MviView = initialIntent() diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt index 5e92ea4a4..41cee99a8 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt @@ -31,6 +31,7 @@ import com.example.android.architecture.blueprints.todoapp.statistics.Statistics import com.example.android.architecture.blueprints.todoapp.util.notOfType import io.reactivex.Observable import io.reactivex.ObservableTransformer +import io.reactivex.Observer import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject @@ -66,9 +67,7 @@ class StatisticsViewModel( } } - override fun processIntents(intents: Observable) { - intents.subscribe(intentsSubject) - } + override val intentsObserver: Observer get() = intentsSubject override fun states(): Observable = statesObservable diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt index 46418748b..a0e5c3922 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt @@ -55,202 +55,202 @@ import kotlin.LazyThreadSafetyMode.NONE * Main UI for the task detail screen. */ class TaskDetailFragment : Fragment(), MviView { - private lateinit var detailTitle: TextView - private lateinit var detailDescription: TextView - private lateinit var detailCompleteStatus: CheckBox - private lateinit var fab: FloatingActionButton - - private val viewModel: TaskDetailViewModel by lazy(NONE) { - ViewModelProviders - .of(this, ToDoViewModelFactory.getInstance(context!!)) - .get(TaskDetailViewModel::class.java) - } - - // Used to manage the data flow lifecycle and avoid memory leak. - private var disposables = CompositeDisposable() - private val deleteTaskIntentPublisher = PublishSubject.create() - - private val argumentTaskId: String - get() = arguments!!.getString(ARGUMENT_TASK_ID) - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - val root = inflater.inflate(R.layout.taskdetail_frag, container, false) - setHasOptionsMenu(true) - detailTitle = root.findViewById(R.id.task_detail_title) as TextView - detailDescription = root.findViewById(R.id.task_detail_description) as TextView - detailCompleteStatus = root.findViewById(R.id.task_detail_complete) as CheckBox - - // Set up floating action button - fab = activity!!.findViewById(R.id.fab_edit_task) as FloatingActionButton - - return root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - bind() - } - - /** - * Connect the [MviView] with the [MviViewModel] - * We subscribe to the [MviViewModel] before passing it the [MviView]'s [MviIntent]s. - * If we were to pass [MviIntent]s to the [MviViewModel] before listening to it, - * emitted [MviViewState]s could be lost - */ - private fun bind() { - // Subscribe to the ViewModel and call render for every emitted state - disposables.add(viewModel.states().subscribe(this::render)) - // Pass the UI's intents to the ViewModel - viewModel.processIntents(intents()) - - // Debounce the FAB clicks to avoid consecutive clicks and navigate to EditTask - RxView.clicks(fab).debounce(200, TimeUnit.MILLISECONDS) - .subscribe { showEditTask(argumentTaskId) } - } - - override fun onDestroy() { - super.onDestroy() - disposables.dispose() - } - - override fun intents(): Observable { - return Observable.merge(initialIntent(), checkBoxIntents(), deleteIntent()) - } - - /** - * The initial Intent the [MviView] emit to convey to the [MviViewModel] - * that it is ready to receive data. - * This initial Intent is also used to pass any parameters the [MviViewModel] might need - * to render the initial [MviViewState] (e.g. the task id to load). - */ - private fun initialIntent(): Observable { - return Observable.just(TaskDetailIntent.InitialIntent(argumentTaskId)) - } - - private fun checkBoxIntents(): Observable { - return RxView.clicks(detailCompleteStatus).map { - if (detailCompleteStatus.isChecked) { - CompleteTaskIntent(argumentTaskId) - } else { - ActivateTaskIntent(argumentTaskId) - } - } - - } - - private fun deleteIntent(): Observable { - return deleteTaskIntentPublisher - } - - override fun render(state: TaskDetailViewState) { - setLoadingIndicator(state.loading) - - if (!state.title.isEmpty()) { - showTitle(state.title) - } else { - hideTitle() - } - - if (!state.description.isEmpty()) { - showDescription(state.description) - } else { - hideDescription() - } - - showActive(state.active) - - when (state.uiNotification) { - TASK_COMPLETE -> showTaskMarkedComplete() - TASK_ACTIVATED -> showTaskMarkedActive() - TASK_DELETED -> activity!!.finish() - null -> { - } - } - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item!!.itemId) { - R.id.menu_delete -> { - deleteTaskIntentPublisher.onNext(TaskDetailIntent.DeleteTask(argumentTaskId)) - return true - } - } - return false - } - - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - inflater!!.inflate(R.menu.taskdetail_fragment_menu, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_EDIT_TASK) { - // If the task was edited successfully, go back to the list. - if (resultCode == Activity.RESULT_OK) { - activity!!.finish() - return - } - } - super.onActivityResult(requestCode, resultCode, data) - } - - fun setLoadingIndicator(active: Boolean) { - if (active) { - detailTitle.text = "" - detailDescription.text = getString(R.string.loading) - } - } - - fun hideDescription() { - detailDescription.visibility = View.GONE - } - - fun hideTitle() { - detailTitle.visibility = View.GONE - } - - fun showActive(isActive: Boolean) { - detailCompleteStatus.isChecked = !isActive - } - - fun showDescription(description: String) { - detailDescription.visibility = View.VISIBLE - detailDescription.text = description - } - - private fun showEditTask(taskId: String) { - val intent = Intent(context, AddEditTaskActivity::class.java) - intent.putExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId) - startActivityForResult(intent, REQUEST_EDIT_TASK) - } - - fun showTaskMarkedComplete() { - Snackbar.make(view!!, getString(R.string.task_marked_complete), Snackbar.LENGTH_LONG) - .show() - } - - fun showTaskMarkedActive() { - Snackbar.make(view!!, getString(R.string.task_marked_active), Snackbar.LENGTH_LONG) - .show() - } - - fun showTitle(title: String) { - detailTitle.visibility = View.VISIBLE - detailTitle.text = title - } - - companion object { - private const val ARGUMENT_TASK_ID = "TASK_ID" - private const val REQUEST_EDIT_TASK = 1 - - operator fun invoke(taskId: String): TaskDetailFragment { - return TaskDetailFragment().apply { - arguments = Bundle().apply { - putString(ARGUMENT_TASK_ID, taskId) - } - } - } - } + private lateinit var detailTitle: TextView + private lateinit var detailDescription: TextView + private lateinit var detailCompleteStatus: CheckBox + private lateinit var fab: FloatingActionButton + + private val viewModel: TaskDetailViewModel by lazy(NONE) { + ViewModelProviders + .of(this, ToDoViewModelFactory.getInstance(context!!)) + .get(TaskDetailViewModel::class.java) + } + + // Used to manage the data flow lifecycle and avoid memory leak. + private var disposables = CompositeDisposable() + private val deleteTaskIntentPublisher = PublishSubject.create() + + private val argumentTaskId: String + get() = arguments!!.getString(ARGUMENT_TASK_ID) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val root = inflater.inflate(R.layout.taskdetail_frag, container, false) + setHasOptionsMenu(true) + detailTitle = root.findViewById(R.id.task_detail_title) as TextView + detailDescription = root.findViewById(R.id.task_detail_description) as TextView + detailCompleteStatus = root.findViewById(R.id.task_detail_complete) as CheckBox + + // Set up floating action button + fab = activity!!.findViewById(R.id.fab_edit_task) as FloatingActionButton + + return root + } + + override fun onStart() { + super.onStart() + bind() + } + + /** + * Connect the [MviView] with the [MviViewModel] + * We subscribe to the [MviViewModel] before passing it the [MviView]'s [MviIntent]s. + * If we were to pass [MviIntent]s to the [MviViewModel] before listening to it, + * emitted [MviViewState]s could be lost + */ + private fun bind() { + disposables.addAll( + // Subscribe to the ViewModel and call render for every emitted state + viewModel.states().subscribe(this::render), + // Pass the UI's intents to the ViewModel + intents().subscribe(viewModel.intentsObserver::onNext), + // Debounce the FAB clicks to avoid consecutive clicks and navigate to EditTask + RxView.clicks(fab).debounce(200, TimeUnit.MILLISECONDS) + .subscribe { showEditTask(argumentTaskId) } + ) + } + + override fun onStop() { + super.onStop() + disposables.clear() + } + + override fun intents(): Observable { + return Observable.merge(initialIntent(), checkBoxIntents(), deleteIntent()) + } + + /** + * The initial Intent the [MviView] emit to convey to the [MviViewModel] + * that it is ready to receive data. + * This initial Intent is also used to pass any parameters the [MviViewModel] might need + * to render the initial [MviViewState] (e.g. the task id to load). + */ + private fun initialIntent(): Observable { + return Observable.just(TaskDetailIntent.InitialIntent(argumentTaskId)) + } + + private fun checkBoxIntents(): Observable { + return RxView.clicks(detailCompleteStatus).map { + if (detailCompleteStatus.isChecked) { + CompleteTaskIntent(argumentTaskId) + } else { + ActivateTaskIntent(argumentTaskId) + } + } + + } + + private fun deleteIntent(): Observable { + return deleteTaskIntentPublisher + } + + override fun render(state: TaskDetailViewState) { + setLoadingIndicator(state.loading) + + if (!state.title.isEmpty()) { + showTitle(state.title) + } else { + hideTitle() + } + + if (!state.description.isEmpty()) { + showDescription(state.description) + } else { + hideDescription() + } + + showActive(state.active) + + when (state.uiNotification) { + TASK_COMPLETE -> showTaskMarkedComplete() + TASK_ACTIVATED -> showTaskMarkedActive() + TASK_DELETED -> activity!!.finish() + null -> { + } + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item!!.itemId) { + R.id.menu_delete -> { + deleteTaskIntentPublisher.onNext(TaskDetailIntent.DeleteTask(argumentTaskId)) + return true + } + } + return false + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + inflater!!.inflate(R.menu.taskdetail_fragment_menu, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_EDIT_TASK) { + // If the task was edited successfully, go back to the list. + if (resultCode == Activity.RESULT_OK) { + activity!!.finish() + return + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + fun setLoadingIndicator(active: Boolean) { + if (active) { + detailTitle.text = "" + detailDescription.text = getString(R.string.loading) + } + } + + fun hideDescription() { + detailDescription.visibility = View.GONE + } + + fun hideTitle() { + detailTitle.visibility = View.GONE + } + + fun showActive(isActive: Boolean) { + detailCompleteStatus.isChecked = !isActive + } + + fun showDescription(description: String) { + detailDescription.visibility = View.VISIBLE + detailDescription.text = description + } + + private fun showEditTask(taskId: String) { + val intent = Intent(context, AddEditTaskActivity::class.java) + intent.putExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId) + startActivityForResult(intent, REQUEST_EDIT_TASK) + } + + fun showTaskMarkedComplete() { + Snackbar.make(view!!, getString(R.string.task_marked_complete), Snackbar.LENGTH_LONG) + .show() + } + + fun showTaskMarkedActive() { + Snackbar.make(view!!, getString(R.string.task_marked_active), Snackbar.LENGTH_LONG) + .show() + } + + fun showTitle(title: String) { + detailTitle.visibility = View.VISIBLE + detailTitle.text = title + } + + companion object { + private const val ARGUMENT_TASK_ID = "TASK_ID" + private const val REQUEST_EDIT_TASK = 1 + + operator fun invoke(taskId: String): TaskDetailFragment { + return TaskDetailFragment().apply { + arguments = Bundle().apply { + putString(ARGUMENT_TASK_ID, taskId) + } + } + } + } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt index 94c5b5c50..d3adb720e 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt @@ -37,6 +37,7 @@ import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetail import com.example.android.architecture.blueprints.todoapp.util.notOfType import io.reactivex.Observable import io.reactivex.ObservableTransformer +import io.reactivex.Observer import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject @@ -72,9 +73,7 @@ class TaskDetailViewModel( } } - override fun processIntents(intents: Observable) { - intents.subscribe(intentsSubject) - } + override val intentsObserver: Observer get() = intentsSubject override fun states(): Observable = statesObservable diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt index 81f33ed58..4573e247c 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt @@ -63,313 +63,308 @@ import kotlin.LazyThreadSafetyMode.NONE * Display a grid of [Task]s. User can choose to view all, active or completed tasks. */ class TasksFragment : Fragment(), MviView { - private lateinit var listAdapter: TasksAdapter - private lateinit var noTasksView: View - private lateinit var noTaskIcon: ImageView - private lateinit var noTaskMainView: TextView - private lateinit var noTaskAddView: TextView - private lateinit var tasksView: LinearLayout - private lateinit var filteringLabelView: TextView - private lateinit var swipeRefreshLayout: ScrollChildSwipeRefreshLayout - private val refreshIntentPublisher = PublishSubject.create() - private val clearCompletedTaskIntentPublisher = - PublishSubject.create() - private val changeFilterIntentPublisher = PublishSubject.create() - // Used to manage the data flow lifecycle and avoid memory leak. - private val disposables = CompositeDisposable() - private val viewModel: TasksViewModel by lazy(NONE) { - ViewModelProviders - .of(this, ToDoViewModelFactory.getInstance(context!!)) - .get(TasksViewModel::class.java) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - listAdapter = TasksAdapter(ArrayList(0)) - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle? - ) { - super.onViewCreated(view, savedInstanceState) - - bind() - } - - /** - * Connect the [MviView] with the [MviViewModel] - * We subscribe to the [MviViewModel] before passing it the [MviView]'s [MviIntent]s. - * If we were to pass [MviIntent]s to the [MviViewModel] before listening to it, - * emitted [MviViewState]s could be lost - */ - private fun bind() { - // Subscribe to the ViewModel and call render for every emitted state - disposables.add(viewModel.states().subscribe(this::render)) - // Pass the UI's intents to the ViewModel - viewModel.processIntents(intents()) - - disposables.add( - listAdapter.taskClickObservable.subscribe { task -> showTaskDetailsUi(task.id) }) - } - - override fun onResume() { - super.onResume() - // conflicting with the initial intent but needed when coming back from the - // AddEditTask activity to refresh the list. - refreshIntentPublisher.onNext(TasksIntent.RefreshIntent(false)) - } - - override fun onDestroy() { - super.onDestroy() - - disposables.dispose() - } - - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent? - ) { - // If a task was successfully added, show snackbar - if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode && Activity.RESULT_OK == resultCode) { - showSuccessfullySavedMessage() - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val root = inflater.inflate(R.layout.tasks_frag, container, false) - - // Set up tasks view - val listView = root.findViewById(R.id.tasks_list) - listView.adapter = listAdapter - filteringLabelView = root.findViewById(R.id.filteringLabel) - tasksView = root.findViewById(R.id.tasksLL) - - // Set up no tasks view - noTasksView = root.findViewById(R.id.noTasks) - noTaskIcon = root.findViewById(R.id.noTasksIcon) - noTaskMainView = root.findViewById(R.id.noTasksMain) - noTaskAddView = root.findViewById(R.id.noTasksAdd) - noTaskAddView.setOnClickListener { showAddTask() } - - // Set up floating action button - val fab = activity!!.findViewById(R.id.fab_add_task) - - fab.setImageResource(R.drawable.ic_add) - fab.setOnClickListener { showAddTask() } - - // Set up progress indicator - swipeRefreshLayout = root.findViewById(R.id.refresh_layout) - swipeRefreshLayout.setColorSchemeColors( - ContextCompat.getColor(activity!!, R.color.colorPrimary), - ContextCompat.getColor(activity!!, R.color.colorAccent), - ContextCompat.getColor(activity!!, R.color.colorPrimaryDark) - ) - // Set the scrolling view in the custom SwipeRefreshLayout. - swipeRefreshLayout.setScrollUpChild(listView) - - setHasOptionsMenu(true) - - return root - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item!!.itemId) { - R.id.menu_clear -> - clearCompletedTaskIntentPublisher.onNext(TasksIntent.ClearCompletedTasksIntent) - R.id.menu_filter -> showFilteringPopUpMenu() - R.id.menu_refresh -> refreshIntentPublisher.onNext(TasksIntent.RefreshIntent(true)) - } - return true - } - - override fun onCreateOptionsMenu( - menu: Menu?, - inflater: MenuInflater? - ) { - inflater!!.inflate(R.menu.tasks_fragment_menu, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun intents(): Observable { - return Observable.merge( - initialIntent(), - refreshIntent(), - adapterIntents(), - clearCompletedTaskIntent() - ) - .mergeWith( - changeFilterIntent() - ) - } - - override fun render(state: TasksViewState) { - swipeRefreshLayout.isRefreshing = state.isLoading - if (state.error != null) { - showLoadingTasksError() - return - } - - when (state.uiNotification) { - TASK_COMPLETE -> showMessage(getString(R.string.task_marked_complete)) - TASK_ACTIVATED -> showMessage(getString(R.string.task_marked_active)) - COMPLETE_TASKS_CLEARED -> showMessage(getString(R.string.completed_tasks_cleared)) - null -> { - } - } - - if (state.tasks.isEmpty()) { - when (state.tasksFilterType) { - ACTIVE_TASKS -> showNoActiveTasks() - COMPLETED_TASKS -> showNoCompletedTasks() - else -> showNoTasks() - } - } else { - listAdapter.replaceData(state.tasks) - - tasksView.visibility = View.VISIBLE - noTasksView.visibility = View.GONE - - when (state.tasksFilterType) { - ACTIVE_TASKS -> showActiveFilterLabel() - COMPLETED_TASKS -> showCompletedFilterLabel() - else -> showAllFilterLabel() - } - } - } - - private fun showFilteringPopUpMenu() { - val popup = PopupMenu(context!!, activity!!.findViewById(R.id.menu_filter)) - popup.menuInflater.inflate(R.menu.filter_tasks, popup.menu) - popup.setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.active -> changeFilterIntentPublisher.onNext( - TasksIntent.ChangeFilterIntent(ACTIVE_TASKS) - ) - R.id.completed -> changeFilterIntentPublisher.onNext( - TasksIntent.ChangeFilterIntent(COMPLETED_TASKS) - ) - else -> changeFilterIntentPublisher.onNext(TasksIntent.ChangeFilterIntent(ALL_TASKS)) - } - true - } - - popup.show() - } - - private fun showMessage(message: String) { - val view = view ?: return - Snackbar.make(view, message, Snackbar.LENGTH_LONG) - .show() - } - - /** - * The initial Intent the [MviView] emit to convey to the [MviViewModel] - * that it is ready to receive data. - * This initial Intent is also used to pass any parameters the [MviViewModel] might need - * to render the initial [MviViewState] (e.g. the task id to load). - */ - private fun initialIntent(): Observable { - return Observable.just(TasksIntent.InitialIntent) - } - - private fun refreshIntent(): Observable { - return RxSwipeRefreshLayout.refreshes(swipeRefreshLayout) - .map { TasksIntent.RefreshIntent(false) } - .mergeWith(refreshIntentPublisher) - } - - private fun clearCompletedTaskIntent(): Observable { - return clearCompletedTaskIntentPublisher - } - - private fun changeFilterIntent(): Observable { - return changeFilterIntentPublisher - } - - private fun adapterIntents(): Observable { - return listAdapter.taskToggleObservable.map { task -> - if (!task.completed) { - CompleteTaskIntent(task) - } else { - ActivateTaskIntent(task) - } - } - } - - private fun showNoActiveTasks() { - showNoTasksViews( - resources.getString(R.string.no_tasks_active), - R.drawable.ic_check_circle_24dp, false - ) - } - - private fun showNoTasks() { - showNoTasksViews( - resources.getString(R.string.no_tasks_all), - R.drawable.ic_assignment_turned_in_24dp, true - ) - } - - private fun showNoCompletedTasks() { - showNoTasksViews( - resources.getString(R.string.no_tasks_completed), - R.drawable.ic_verified_user_24dp, false - ) - } - - private fun showSuccessfullySavedMessage() { - showMessage(getString(R.string.successfully_saved_task_message)) - } - - private fun showNoTasksViews( - mainText: String, - iconRes: Int, - showAddView: Boolean - ) { - tasksView.visibility = View.GONE - noTasksView.visibility = View.VISIBLE - - noTaskMainView.text = mainText - noTaskIcon.setImageDrawable(ContextCompat.getDrawable(context!!, iconRes)) - noTaskAddView.visibility = if (showAddView) View.VISIBLE else View.GONE - } - - private fun showActiveFilterLabel() { - filteringLabelView.text = resources.getString(R.string.label_active) - } - - private fun showCompletedFilterLabel() { - filteringLabelView.text = resources.getString(R.string.label_completed) - } - - private fun showAllFilterLabel() { - filteringLabelView.text = resources.getString(R.string.label_all) - } - - private fun showAddTask() { - val intent = Intent(context, AddEditTaskActivity::class.java) - startActivityForResult(intent, AddEditTaskActivity.REQUEST_ADD_TASK) - } - - private fun showTaskDetailsUi(taskId: String) { - // in it's own Activity, since it makes more sense that way and it gives us the flexibility - // to show some MviIntent stubbing. - val intent = Intent(context, TaskDetailActivity::class.java) - intent.putExtra(TaskDetailActivity.EXTRA_TASK_ID, taskId) - startActivity(intent) - } - - private fun showLoadingTasksError() { - showMessage(getString(R.string.loading_tasks_error)) - } - - companion object { - operator fun invoke(): TasksFragment = TasksFragment() - } + private lateinit var listAdapter: TasksAdapter + private lateinit var noTasksView: View + private lateinit var noTaskIcon: ImageView + private lateinit var noTaskMainView: TextView + private lateinit var noTaskAddView: TextView + private lateinit var tasksView: LinearLayout + private lateinit var filteringLabelView: TextView + private lateinit var swipeRefreshLayout: ScrollChildSwipeRefreshLayout + private val refreshIntentPublisher = PublishSubject.create() + private val clearCompletedTaskIntentPublisher = + PublishSubject.create() + private val changeFilterIntentPublisher = PublishSubject.create() + // Used to manage the data flow lifecycle and avoid memory leak. + private val disposables = CompositeDisposable() + private val viewModel: TasksViewModel by lazy(NONE) { + ViewModelProviders + .of(this, ToDoViewModelFactory.getInstance(context!!)) + .get(TasksViewModel::class.java) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + listAdapter = TasksAdapter(ArrayList(0)) + } + + override fun onStart() { + super.onStart() + bind() + } + + /** + * Connect the [MviView] with the [MviViewModel] + * We subscribe to the [MviViewModel] before passing it the [MviView]'s [MviIntent]s. + * If we were to pass [MviIntent]s to the [MviViewModel] before listening to it, + * emitted [MviViewState]s could be lost + */ + private fun bind() { + disposables.addAll( + // Subscribe to the ViewModel and call render for every emitted state + viewModel.states().subscribe(this::render), + // Pass the UI's intents to the ViewModel + intents().subscribe(viewModel.intentsObserver::onNext), + listAdapter.taskClickObservable.subscribe { task -> showTaskDetailsUi(task.id) } + ) + } + + override fun onResume() { + super.onResume() + // conflicting with the initial intent but needed when coming back from the + // AddEditTask activity to refresh the list. + refreshIntentPublisher.onNext(TasksIntent.RefreshIntent(false)) + } + + override fun onStop() { + super.onStop() + disposables.clear() + } + + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent? + ) { + // If a task was successfully added, show snackbar + if (AddEditTaskActivity.REQUEST_ADD_TASK == requestCode && Activity.RESULT_OK == resultCode) { + showSuccessfullySavedMessage() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val root = inflater.inflate(R.layout.tasks_frag, container, false) + + // Set up tasks view + val listView = root.findViewById(R.id.tasks_list) + listView.adapter = listAdapter + filteringLabelView = root.findViewById(R.id.filteringLabel) + tasksView = root.findViewById(R.id.tasksLL) + + // Set up no tasks view + noTasksView = root.findViewById(R.id.noTasks) + noTaskIcon = root.findViewById(R.id.noTasksIcon) + noTaskMainView = root.findViewById(R.id.noTasksMain) + noTaskAddView = root.findViewById(R.id.noTasksAdd) + noTaskAddView.setOnClickListener { showAddTask() } + + // Set up floating action button + val fab = activity!!.findViewById(R.id.fab_add_task) + + fab.setImageResource(R.drawable.ic_add) + fab.setOnClickListener { showAddTask() } + + // Set up progress indicator + swipeRefreshLayout = root.findViewById(R.id.refresh_layout) + swipeRefreshLayout.setColorSchemeColors( + ContextCompat.getColor(activity!!, R.color.colorPrimary), + ContextCompat.getColor(activity!!, R.color.colorAccent), + ContextCompat.getColor(activity!!, R.color.colorPrimaryDark) + ) + // Set the scrolling view in the custom SwipeRefreshLayout. + swipeRefreshLayout.setScrollUpChild(listView) + + setHasOptionsMenu(true) + + return root + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item!!.itemId) { + R.id.menu_clear -> + clearCompletedTaskIntentPublisher.onNext(TasksIntent.ClearCompletedTasksIntent) + R.id.menu_filter -> showFilteringPopUpMenu() + R.id.menu_refresh -> refreshIntentPublisher.onNext(TasksIntent.RefreshIntent(true)) + } + return true + } + + override fun onCreateOptionsMenu( + menu: Menu?, + inflater: MenuInflater? + ) { + inflater!!.inflate(R.menu.tasks_fragment_menu, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun intents(): Observable { + return Observable.merge( + initialIntent(), + refreshIntent(), + adapterIntents(), + clearCompletedTaskIntent() + ) + .mergeWith( + changeFilterIntent() + ) + } + + override fun render(state: TasksViewState) { + swipeRefreshLayout.isRefreshing = state.isLoading + if (state.error != null) { + showLoadingTasksError() + return + } + + when (state.uiNotification) { + TASK_COMPLETE -> showMessage(getString(R.string.task_marked_complete)) + TASK_ACTIVATED -> showMessage(getString(R.string.task_marked_active)) + COMPLETE_TASKS_CLEARED -> showMessage(getString(R.string.completed_tasks_cleared)) + null -> { + } + } + + if (state.tasks.isEmpty()) { + when (state.tasksFilterType) { + ACTIVE_TASKS -> showNoActiveTasks() + COMPLETED_TASKS -> showNoCompletedTasks() + else -> showNoTasks() + } + } else { + listAdapter.replaceData(state.tasks) + + tasksView.visibility = View.VISIBLE + noTasksView.visibility = View.GONE + + when (state.tasksFilterType) { + ACTIVE_TASKS -> showActiveFilterLabel() + COMPLETED_TASKS -> showCompletedFilterLabel() + else -> showAllFilterLabel() + } + } + } + + private fun showFilteringPopUpMenu() { + val popup = PopupMenu(context!!, activity!!.findViewById(R.id.menu_filter)) + popup.menuInflater.inflate(R.menu.filter_tasks, popup.menu) + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.active -> changeFilterIntentPublisher.onNext( + TasksIntent.ChangeFilterIntent(ACTIVE_TASKS) + ) + R.id.completed -> changeFilterIntentPublisher.onNext( + TasksIntent.ChangeFilterIntent(COMPLETED_TASKS) + ) + else -> changeFilterIntentPublisher.onNext(TasksIntent.ChangeFilterIntent(ALL_TASKS)) + } + true + } + + popup.show() + } + + private fun showMessage(message: String) { + val view = view ?: return + Snackbar.make(view, message, Snackbar.LENGTH_LONG) + .show() + } + + /** + * The initial Intent the [MviView] emit to convey to the [MviViewModel] + * that it is ready to receive data. + * This initial Intent is also used to pass any parameters the [MviViewModel] might need + * to render the initial [MviViewState] (e.g. the task id to load). + */ + private fun initialIntent(): Observable { + return Observable.just(TasksIntent.InitialIntent) + } + + private fun refreshIntent(): Observable { + return RxSwipeRefreshLayout.refreshes(swipeRefreshLayout) + .map { TasksIntent.RefreshIntent(false) } + .mergeWith(refreshIntentPublisher) + } + + private fun clearCompletedTaskIntent(): Observable { + return clearCompletedTaskIntentPublisher + } + + private fun changeFilterIntent(): Observable { + return changeFilterIntentPublisher + } + + private fun adapterIntents(): Observable { + return listAdapter.taskToggleObservable.map { task -> + if (!task.completed) { + CompleteTaskIntent(task) + } else { + ActivateTaskIntent(task) + } + } + } + + private fun showNoActiveTasks() { + showNoTasksViews( + resources.getString(R.string.no_tasks_active), + R.drawable.ic_check_circle_24dp, false + ) + } + + private fun showNoTasks() { + showNoTasksViews( + resources.getString(R.string.no_tasks_all), + R.drawable.ic_assignment_turned_in_24dp, true + ) + } + + private fun showNoCompletedTasks() { + showNoTasksViews( + resources.getString(R.string.no_tasks_completed), + R.drawable.ic_verified_user_24dp, false + ) + } + + private fun showSuccessfullySavedMessage() { + showMessage(getString(R.string.successfully_saved_task_message)) + } + + private fun showNoTasksViews( + mainText: String, + iconRes: Int, + showAddView: Boolean + ) { + tasksView.visibility = View.GONE + noTasksView.visibility = View.VISIBLE + + noTaskMainView.text = mainText + noTaskIcon.setImageDrawable(ContextCompat.getDrawable(context!!, iconRes)) + noTaskAddView.visibility = if (showAddView) View.VISIBLE else View.GONE + } + + private fun showActiveFilterLabel() { + filteringLabelView.text = resources.getString(R.string.label_active) + } + + private fun showCompletedFilterLabel() { + filteringLabelView.text = resources.getString(R.string.label_completed) + } + + private fun showAllFilterLabel() { + filteringLabelView.text = resources.getString(R.string.label_all) + } + + private fun showAddTask() { + val intent = Intent(context, AddEditTaskActivity::class.java) + startActivityForResult(intent, AddEditTaskActivity.REQUEST_ADD_TASK) + } + + private fun showTaskDetailsUi(taskId: String) { + // in it's own Activity, since it makes more sense that way and it gives us the flexibility + // to show some MviIntent stubbing. + val intent = Intent(context, TaskDetailActivity::class.java) + intent.putExtra(TaskDetailActivity.EXTRA_TASK_ID, taskId) + startActivity(intent) + } + + private fun showLoadingTasksError() { + showMessage(getString(R.string.loading_tasks_error)) + } + + companion object { + operator fun invoke(): TasksFragment = TasksFragment() + } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt index ae921500e..3e488cd9c 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt @@ -41,6 +41,7 @@ import com.example.android.architecture.blueprints.todoapp.tasks.TasksViewState. import com.example.android.architecture.blueprints.todoapp.util.notOfType import io.reactivex.Observable import io.reactivex.ObservableTransformer +import io.reactivex.Observer import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject @@ -77,9 +78,7 @@ class TasksViewModel( } } - override fun processIntents(intents: Observable) { - intents.subscribe(intentsSubject) - } + override val intentsObserver: Observer get() = intentsSubject override fun states(): Observable = statesObservable