From b0bb58bb4cfa4d9b840bed21926df4f67373ec73 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sat, 3 Jun 2023 01:58:15 -0500 Subject: [PATCH] Convert TaskListViewModel to flow --- .../astrid/activity/TaskEditFragment.kt | 55 +++++++------ .../astrid/activity/TaskListFragment.kt | 67 +++++++++++----- .../java/org/tasks/compose/FlowHelpers.kt | 10 +++ .../java/org/tasks/ui/SubtaskControlSet.kt | 3 +- .../java/org/tasks/ui/TaskListViewModel.kt | 80 ++++++++++--------- 5 files changed, 131 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt index 19e10b5e0..5431f0c77 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt @@ -6,7 +6,6 @@ package com.todoroo.astrid.activity import android.app.Activity -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.graphics.Paint @@ -16,15 +15,20 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.Toolbar import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Divider -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput @@ -55,19 +59,32 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.analytics.Firebase import org.tasks.calendars.CalendarPicker import org.tasks.compose.BeastModeBanner import org.tasks.compose.collectAsStateLifecycleAware -import org.tasks.compose.edit.* +import org.tasks.compose.edit.CommentsRow +import org.tasks.compose.edit.DescriptionRow +import org.tasks.compose.edit.DueDateRow +import org.tasks.compose.edit.InfoRow +import org.tasks.compose.edit.ListRow +import org.tasks.compose.edit.PriorityRow import org.tasks.data.Alarm import org.tasks.data.Location import org.tasks.data.TagData import org.tasks.data.UserActivityDao -import org.tasks.databinding.* +import org.tasks.databinding.FragmentTaskEditBinding +import org.tasks.databinding.TaskEditCalendarBinding +import org.tasks.databinding.TaskEditFilesBinding +import org.tasks.databinding.TaskEditLocationBinding +import org.tasks.databinding.TaskEditRemindersBinding +import org.tasks.databinding.TaskEditRepeatBinding +import org.tasks.databinding.TaskEditStartDateBinding +import org.tasks.databinding.TaskEditSubtasksBinding +import org.tasks.databinding.TaskEditTagsBinding +import org.tasks.databinding.TaskEditTimerBinding import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DialogBuilder @@ -84,13 +101,18 @@ import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_PRIOR import org.tasks.markdown.MarkdownProvider import org.tasks.notifications.NotificationManager import org.tasks.preferences.Preferences -import org.tasks.ui.* +import org.tasks.ui.CalendarControlSet +import org.tasks.ui.ChipProvider +import org.tasks.ui.LocationControlSet +import org.tasks.ui.SubtaskControlSet +import org.tasks.ui.TaskEditEvent +import org.tasks.ui.TaskEditEventBus +import org.tasks.ui.TaskEditViewModel import org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns import java.time.format.FormatStyle -import java.util.* +import java.util.Locale import javax.inject.Inject import kotlin.math.abs -import android.view.inputmethod.EditorInfo @AndroidEntryPoint class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { @@ -106,15 +128,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { @Inject lateinit var linkify: Linkify @Inject lateinit var markdownProvider: MarkdownProvider @Inject lateinit var taskEditEventBus: TaskEditEventBus - @Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var locale: Locale @Inject lateinit var chipProvider: ChipProvider val editViewModel: TaskEditViewModel by viewModels() - val subtaskViewModel: TaskListViewModel by viewModels() lateinit var binding: FragmentTaskEditBinding private var showKeyboard = false - private val refreshReceiver = RefreshReceiver() private val beastMode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activity?.recreate() @@ -317,7 +336,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { override fun onResume() { super.onResume() - localBroadcastManager.registerRefreshReceiver(refreshReceiver) if (showKeyboard) { binding.title.requestFocus() val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager @@ -328,11 +346,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } } - override fun onPause() { - super.onPause() - localBroadcastManager.unregisterReceiver(refreshReceiver) - } - override fun onMenuItemClick(item: MenuItem): Boolean { AndroidUtilities.hideKeyboard(activity) if (item.itemId == R.id.menu_delete) { @@ -417,12 +430,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { } } - private inner class RefreshReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - subtaskViewModel.invalidate() - } - } - @Composable private fun DueDateRow() { val dueDate = editViewModel.dueDate.collectAsStateLifecycleAware().value diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index aa29dfc5d..0d8bccd14 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -12,7 +12,12 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.speech.RecognizerIntent -import android.view.* +import android.view.Gravity +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode @@ -30,7 +35,9 @@ import androidx.core.view.isVisible import androidx.core.view.setMargins import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -45,20 +52,33 @@ import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.astrid.adapter.TaskAdapter import com.todoroo.astrid.adapter.TaskAdapterProvider -import com.todoroo.astrid.api.* import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID +import com.todoroo.astrid.api.CaldavFilter +import com.todoroo.astrid.api.Filter +import com.todoroo.astrid.api.GtasksFilter +import com.todoroo.astrid.api.IdListFilter +import com.todoroo.astrid.api.SearchFilter +import com.todoroo.astrid.api.TagFilter import com.todoroo.astrid.core.BuiltInFilterExposer import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.data.Task import com.todoroo.astrid.repeats.RepeatTaskHelper -import com.todoroo.astrid.service.* +import com.todoroo.astrid.service.TaskCompleter +import com.todoroo.astrid.service.TaskCreator +import com.todoroo.astrid.service.TaskDeleter +import com.todoroo.astrid.service.TaskDuplicator +import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.timers.TimerPlugin import com.todoroo.astrid.utility.Flags import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.ShortcutManager @@ -93,12 +113,18 @@ import org.tasks.preferences.Device import org.tasks.preferences.Preferences import org.tasks.sync.SyncAdapters import org.tasks.tags.TagPickerActivity -import org.tasks.tasklist.* +import org.tasks.tasklist.DragAndDropRecyclerAdapter +import org.tasks.tasklist.TaskViewHolder +import org.tasks.tasklist.ViewHolderFactory import org.tasks.themes.ColorProvider import org.tasks.themes.ThemeColor -import org.tasks.ui.* +import org.tasks.ui.TaskEditEvent +import org.tasks.ui.TaskEditEventBus +import org.tasks.ui.TaskListEvent +import org.tasks.ui.TaskListEventBus +import org.tasks.ui.TaskListViewModel import java.time.format.FormatStyle -import java.util.* +import java.util.Locale import javax.inject.Inject import kotlin.math.max @@ -263,14 +289,18 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL listViewModel.setFilter((if (searchQuery == null) filter else createSearchFilter(searchQuery!!))) (recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false recyclerView.layoutManager = LinearLayoutManager(context) - listViewModel.observe(this) { - submitList(it) - if (it.isEmpty()) { - swipeRefreshLayout.visibility = View.GONE - emptyRefreshLayout.visibility = View.VISIBLE - } else { - swipeRefreshLayout.visibility = View.VISIBLE - emptyRefreshLayout.visibility = View.GONE + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + listViewModel.tasks.collect { + submitList(it) + if (it.isEmpty()) { + swipeRefreshLayout.visibility = View.GONE + emptyRefreshLayout.visibility = View.VISIBLE + } else { + swipeRefreshLayout.visibility = View.VISIBLE + emptyRefreshLayout.visibility = View.GONE + } + } } } setupRefresh(swipeRefreshLayout) @@ -383,11 +413,11 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL delay(SEARCH_DEBOUNCE_TIMEOUT) searchQuery = query?.trim { it <= ' ' } ?: "" if (searchQuery?.isEmpty() == true) { - listViewModel.searchByFilter( + listViewModel.setFilter( BuiltInFilterExposer.getMyTasksFilter(requireContext().resources)) } else { val savedFilter = createSearchFilter(searchQuery!!) - listViewModel.searchByFilter(savedFilter) + listViewModel.setFilter(savedFilter) } } } @@ -572,7 +602,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } private fun refresh() { - loadTaskListContent() setSyncOngoing() } @@ -666,7 +695,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL override fun onMenuItemActionCollapse(item: MenuItem): Boolean { search.setOnQueryTextListener(null) - listViewModel.searchByFilter(filter) + listViewModel.setFilter(filter) searchJob?.cancel() searchQuery = null if (preferences.isTopAppBar) { diff --git a/app/src/main/java/org/tasks/compose/FlowHelpers.kt b/app/src/main/java/org/tasks/compose/FlowHelpers.kt index ba213180d..f4fb264c1 100644 --- a/app/src/main/java/org/tasks/compose/FlowHelpers.kt +++ b/app/src/main/java/org/tasks/compose/FlowHelpers.kt @@ -8,11 +8,21 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.flowWithLifecycle +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flow import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +fun Flow.throttleLatest(period: Long) = flow { + conflate().collect { + emit(it) + delay(period) + } +} + // https://proandroiddev.com/how-to-collect-flows-lifecycle-aware-in-jetpack-compose-babd53582d0b @Composable diff --git a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt index 56941f439..81261f14a 100644 --- a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt +++ b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.os.Bundle import android.view.View import android.view.ViewGroup -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -59,7 +58,7 @@ class SubtaskControlSet : TaskEditControlFragment() { filter = viewModel.selectedList.collectAsStateLifecycleAware().value, hasParent = viewModel.hasParent, desaturate = preferences.desaturateDarkMode, - existingSubtasks = listViewModel.tasks.observeAsState(initial = emptyList()).value, + existingSubtasks = listViewModel.tasks.collectAsStateLifecycleAware(initial = emptyList()).value, newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value, openSubtask = this@SubtaskControlSet::openSubtask, completeExistingSubtask = this@SubtaskControlSet::complete, diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt index 4bcace46f..542a8c5f0 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt @@ -1,64 +1,66 @@ package org.tasks.ui -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.todoroo.andlib.utility.AndroidUtilities +import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.api.Filter import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import org.tasks.LocalBroadcastManager +import org.tasks.compose.throttleLatest import org.tasks.data.TaskContainer import org.tasks.data.TaskDao import org.tasks.data.TaskListQuery.getQuery import org.tasks.preferences.Preferences -import timber.log.Timber import javax.inject.Inject @HiltViewModel class TaskListViewModel @Inject constructor( private val preferences: Preferences, - private val taskDao: TaskDao) : ViewModel() { + private val taskDao: TaskDao, + private val localBroadcastManager: LocalBroadcastManager, +) : ViewModel() { - private var _tasks = MutableLiveData>() - val tasks: LiveData> - get() = _tasks - private var filter: Filter? = null - private var manualSortFilter = false + data class State( + val filter: Filter? = null, + val now: Long = DateUtilities.now(), + ) - fun setFilter(filter: Filter) { - manualSortFilter = (filter.supportsManualSort() && preferences.isManualSort - || filter.supportsAstridSorting() && preferences.isAstridSort) - if (filter != this.filter || filter.getSqlQuery() != this.filter!!.getSqlQuery()) { - this.filter = filter - _tasks = MutableLiveData() + private val _state = MutableStateFlow(State()) + + val tasks: Flow> = + _state + .filter { it.filter != null } + .throttleLatest(333) + .map { taskDao.fetchTasks { getQuery(preferences, it.filter!!) } } + + private val refreshReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { invalidate() } } - fun observe(owner: LifecycleOwner, observer: (List) -> Unit) = - _tasks.observe(owner, observer) - - fun searchByFilter(filter: Filter?) { - this.filter = filter - invalidate() + fun setFilter(filter: Filter) { + _state.update { + it.copy(filter = filter) + } } fun invalidate() { - AndroidUtilities.assertMainThread() - if (filter == null) { - return - } - try { - viewModelScope.launch { - _tasks.value = taskDao.fetchTasks { getQuery(preferences, filter!!) } - } - } catch (e: Exception) { - Timber.e(e) - } + _state.update { it.copy(now = DateUtilities.now()) } } - val value: List - get() = _tasks.value ?: emptyList() -} \ No newline at end of file + init { + localBroadcastManager.registerRefreshReceiver(refreshReceiver) + } + + override fun onCleared() { + localBroadcastManager.unregisterReceiver(refreshReceiver) + } +}