Convert TaskListViewModel to flow

pull/2349/head
Alex Baker 1 year ago
parent 1811eb561f
commit b0bb58bb4c

@ -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

@ -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) {

@ -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 <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
@Composable

@ -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,

@ -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<List<TaskContainer>>()
val tasks: LiveData<List<TaskContainer>>
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<List<TaskContainer>> =
_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<TaskContainer>) -> 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<TaskContainer>
get() = _tasks.value ?: emptyList()
}
init {
localBroadcastManager.registerRefreshReceiver(refreshReceiver)
}
override fun onCleared() {
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
}

Loading…
Cancel
Save