Convert TaskListViewModel to flow

pull/2349/head
Alex Baker 3 years ago
parent 1811eb561f
commit b0bb58bb4c

@ -6,7 +6,6 @@
package com.todoroo.astrid.activity package com.todoroo.astrid.activity
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Paint import android.graphics.Paint
@ -16,15 +15,20 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi 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.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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput 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.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.calendars.CalendarPicker import org.tasks.calendars.CalendarPicker
import org.tasks.compose.BeastModeBanner import org.tasks.compose.BeastModeBanner
import org.tasks.compose.collectAsStateLifecycleAware 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.Alarm
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.TagData import org.tasks.data.TagData
import org.tasks.data.UserActivityDao 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.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DialogBuilder 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.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Preferences 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 org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.* import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
import android.view.inputmethod.EditorInfo
@AndroidEntryPoint @AndroidEntryPoint
class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@ -106,15 +128,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var linkify: Linkify @Inject lateinit var linkify: Linkify
@Inject lateinit var markdownProvider: MarkdownProvider @Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var taskEditEventBus: TaskEditEventBus @Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
val editViewModel: TaskEditViewModel by viewModels() val editViewModel: TaskEditViewModel by viewModels()
val subtaskViewModel: TaskListViewModel by viewModels()
lateinit var binding: FragmentTaskEditBinding lateinit var binding: FragmentTaskEditBinding
private var showKeyboard = false private var showKeyboard = false
private val refreshReceiver = RefreshReceiver()
private val beastMode = private val beastMode =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
activity?.recreate() activity?.recreate()
@ -317,7 +336,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
localBroadcastManager.registerRefreshReceiver(refreshReceiver)
if (showKeyboard) { if (showKeyboard) {
binding.title.requestFocus() binding.title.requestFocus()
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 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 { override fun onMenuItemClick(item: MenuItem): Boolean {
AndroidUtilities.hideKeyboard(activity) AndroidUtilities.hideKeyboard(activity)
if (item.itemId == R.id.menu_delete) { 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 @Composable
private fun DueDateRow() { private fun DueDateRow() {
val dueDate = editViewModel.dueDate.collectAsStateLifecycleAware().value val dueDate = editViewModel.dueDate.collectAsStateLifecycleAware().value

@ -12,7 +12,12 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.speech.RecognizerIntent 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.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@ -30,7 +35,9 @@ import androidx.core.view.isVisible
import androidx.core.view.setMargins import androidx.core.view.setMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.andlib.utility.DateUtilities.now
import com.todoroo.astrid.adapter.TaskAdapter import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider 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_OLD_DUE_DATE
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID 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.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.repeats.RepeatTaskHelper 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.timers.TimerPlugin
import com.todoroo.astrid.utility.Flags import com.todoroo.astrid.utility.Flags
import dagger.hilt.android.AndroidEntryPoint 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.ShortcutManager import org.tasks.ShortcutManager
@ -93,12 +113,18 @@ import org.tasks.preferences.Device
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
import org.tasks.tags.TagPickerActivity 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.ColorProvider
import org.tasks.themes.ThemeColor 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.time.format.FormatStyle
import java.util.* import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
@ -263,14 +289,18 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
listViewModel.setFilter((if (searchQuery == null) filter else createSearchFilter(searchQuery!!))) listViewModel.setFilter((if (searchQuery == null) filter else createSearchFilter(searchQuery!!)))
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false (recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.layoutManager = LinearLayoutManager(context)
listViewModel.observe(this) { lifecycleScope.launch {
submitList(it) viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
if (it.isEmpty()) { listViewModel.tasks.collect {
swipeRefreshLayout.visibility = View.GONE submitList(it)
emptyRefreshLayout.visibility = View.VISIBLE if (it.isEmpty()) {
} else { swipeRefreshLayout.visibility = View.GONE
swipeRefreshLayout.visibility = View.VISIBLE emptyRefreshLayout.visibility = View.VISIBLE
emptyRefreshLayout.visibility = View.GONE } else {
swipeRefreshLayout.visibility = View.VISIBLE
emptyRefreshLayout.visibility = View.GONE
}
}
} }
} }
setupRefresh(swipeRefreshLayout) setupRefresh(swipeRefreshLayout)
@ -383,11 +413,11 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
delay(SEARCH_DEBOUNCE_TIMEOUT) delay(SEARCH_DEBOUNCE_TIMEOUT)
searchQuery = query?.trim { it <= ' ' } ?: "" searchQuery = query?.trim { it <= ' ' } ?: ""
if (searchQuery?.isEmpty() == true) { if (searchQuery?.isEmpty() == true) {
listViewModel.searchByFilter( listViewModel.setFilter(
BuiltInFilterExposer.getMyTasksFilter(requireContext().resources)) BuiltInFilterExposer.getMyTasksFilter(requireContext().resources))
} else { } else {
val savedFilter = createSearchFilter(searchQuery!!) val savedFilter = createSearchFilter(searchQuery!!)
listViewModel.searchByFilter(savedFilter) listViewModel.setFilter(savedFilter)
} }
} }
} }
@ -572,7 +602,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
private fun refresh() { private fun refresh() {
loadTaskListContent()
setSyncOngoing() setSyncOngoing()
} }
@ -666,7 +695,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
search.setOnQueryTextListener(null) search.setOnQueryTextListener(null)
listViewModel.searchByFilter(filter) listViewModel.setFilter(filter)
searchJob?.cancel() searchJob?.cancel()
searchQuery = null searchQuery = null
if (preferences.isTopAppBar) { if (preferences.isTopAppBar) {

@ -8,11 +8,21 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
fun <T> Flow<T>.throttleLatest(period: Long) = flow {
conflate().collect {
emit(it)
delay(period)
}
}
// https://proandroiddev.com/how-to-collect-flows-lifecycle-aware-in-jetpack-compose-babd53582d0b // https://proandroiddev.com/how-to-collect-flows-lifecycle-aware-in-jetpack-compose-babd53582d0b
@Composable @Composable

@ -4,7 +4,6 @@ import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -59,7 +58,7 @@ class SubtaskControlSet : TaskEditControlFragment() {
filter = viewModel.selectedList.collectAsStateLifecycleAware().value, filter = viewModel.selectedList.collectAsStateLifecycleAware().value,
hasParent = viewModel.hasParent, hasParent = viewModel.hasParent,
desaturate = preferences.desaturateDarkMode, desaturate = preferences.desaturateDarkMode,
existingSubtasks = listViewModel.tasks.observeAsState(initial = emptyList()).value, existingSubtasks = listViewModel.tasks.collectAsStateLifecycleAware(initial = emptyList()).value,
newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value, newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value,
openSubtask = this@SubtaskControlSet::openSubtask, openSubtask = this@SubtaskControlSet::openSubtask,
completeExistingSubtask = this@SubtaskControlSet::complete, completeExistingSubtask = this@SubtaskControlSet::complete,

@ -1,64 +1,66 @@
package org.tasks.ui package org.tasks.ui
import androidx.lifecycle.LifecycleOwner import android.content.BroadcastReceiver
import androidx.lifecycle.LiveData import android.content.Context
import androidx.lifecycle.MutableLiveData import android.content.Intent
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import dagger.hilt.android.lifecycle.HiltViewModel 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.TaskContainer
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TaskListViewModel @Inject constructor( class TaskListViewModel @Inject constructor(
private val preferences: Preferences, private val preferences: Preferences,
private val taskDao: TaskDao) : ViewModel() { private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
) : ViewModel() {
private var _tasks = MutableLiveData<List<TaskContainer>>() data class State(
val tasks: LiveData<List<TaskContainer>> val filter: Filter? = null,
get() = _tasks val now: Long = DateUtilities.now(),
private var filter: Filter? = null )
private var manualSortFilter = false
fun setFilter(filter: Filter) { private val _state = MutableStateFlow(State())
manualSortFilter = (filter.supportsManualSort() && preferences.isManualSort
|| filter.supportsAstridSorting() && preferences.isAstridSort) val tasks: Flow<List<TaskContainer>> =
if (filter != this.filter || filter.getSqlQuery() != this.filter!!.getSqlQuery()) { _state
this.filter = filter .filter { it.filter != null }
_tasks = MutableLiveData() .throttleLatest(333)
.map { taskDao.fetchTasks { getQuery(preferences, it.filter!!) } }
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
invalidate() invalidate()
} }
} }
fun observe(owner: LifecycleOwner, observer: (List<TaskContainer>) -> Unit) = fun setFilter(filter: Filter) {
_tasks.observe(owner, observer) _state.update {
it.copy(filter = filter)
fun searchByFilter(filter: Filter?) { }
this.filter = filter
invalidate()
} }
fun invalidate() { fun invalidate() {
AndroidUtilities.assertMainThread() _state.update { it.copy(now = DateUtilities.now()) }
if (filter == null) {
return
}
try {
viewModelScope.launch {
_tasks.value = taskDao.fetchTasks { getQuery(preferences, filter!!) }
}
} catch (e: Exception) {
Timber.e(e)
}
} }
val value: List<TaskContainer> init {
get() = _tasks.value ?: emptyList() localBroadcastManager.registerRefreshReceiver(refreshReceiver)
} }
override fun onCleared() {
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
}

Loading…
Cancel
Save