I have the following database structure:
Task
can be of two types: Day
or Period
. If it is of type Day
it will have only one row in Task_Completion
(task lasts only within that day) while if it is of type Period
you have as many Task_Completion
rows as the duration span of the task (e.g. 10th July – 29th July –> 9 rows in Task_Completion
).
My app has only a weekly view of tasks, so I am only concerned about tasks in selected week.
Still, I have a problem with Period
tasks: I cannot create all the Task_Completion
rows in advance (e.g. it can last 10 years, so I can’t insert 3650 rows at once with the user waiting for it).
My workaround for this was to insert the Task_Completion
rows only for the selected week, than retrieve them to show the UI, but I don’t know how to do it on Jetpack Compose using suspend
queries which return Flow
s. I have also tried with a Transactional
function, but it ran on main thread if I called it from the ViewModel
This is the code I’ve tried so far:
TaskDao
package com.pochopsp.dailytasks.data.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import com.pochopsp.dailytasks.data.database.entity.Completion
import com.pochopsp.dailytasks.data.database.entity.Task
import com.pochopsp.dailytasks.data.database.entity.TaskPeriod
import com.pochopsp.dailytasks.domain.date.DatePeriod
import com.pochopsp.dailytasks.domain.task.TaskCardDto
import kotlinx.coroutines.flow.Flow
import java.util.Date
@Dao
interface TaskDao {
@Query("SELECT * FROM task INNER JOIN period ON task.id = period.taskId" +
" INNER JOIN completion ON task.id = completion.taskId" +
" WHERE date(startDate / 1000,'unixepoch') >= date(:weekStart / 1000,'unixepoch')" +
" AND date(startDate / 1000,'unixepoch') <= date(:weekEnd / 1000,'unixepoch')"
)
fun getPeriodTasksForWeek(weekStart: Date, weekEnd: Date): List<TaskCardDto>
@Query("SELECT * FROM Task t INNER JOIN Period p" +
" WHERE date(:weekStart / 1000,'unixepoch') >= date(t.startDate / 1000,'unixepoch')" +
" AND t.id NOT IN " +
"(SELECT c.taskId FROM completion c" +
" WHERE date(c.date / 1000,'unixepoch') >= date(:weekStart / 1000,'unixepoch')" +
" AND date(c.date / 1000,'unixepoch') <= date(:weekEnd / 1000,'unixepoch'))"
)
fun getTaskPeriodsWithoutCompletionForWeek(weekStart: Date, weekEnd: Date): List<TaskPeriod>
@Transaction
fun insertAndRetrieve(weekStart: Date, weekEnd: Date, completionDao: CompletionDao) : List<TaskCardDto> {
// retrieve period tasks without a completion
val taskPeriods = getTaskPeriodsWithoutCompletionForWeek(weekStart, weekEnd);
// insert missing completions for them
for(taskPeriod in taskPeriods){
val periodStart = if(weekStart.before(taskPeriod.task.startDate)) weekStart else taskPeriod.task.startDate
val periodEnd = if(taskPeriod.period.endDate.before(weekEnd)) taskPeriod.period.endDate else weekEnd
val datesInInterval: List<Date> = DatePeriod(start = periodStart, end = periodEnd).getDatesBetween()
for(date in datesInInterval)
completionDao.upsertCompletionSync(Completion(taskId = taskPeriod.task.id, date = date))
}
// retrieve them
return getPeriodTasksForWeek(weekStart = weekStart, weekEnd = weekEnd)
}
}
Task
package com.pochopsp.dailytasks.data.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.pochopsp.dailytasks.domain.task.TaskType
import java.util.Date
@Entity
data class Task(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val icon: String? = null,
val notes: String? = null,
val type: TaskType? = null,
val startDate: Date,
val notificationTime: String? = null,
val repeatNotification: String? = null
)
Period
package com.pochopsp.dailytasks.data.database.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.Date
@Entity(foreignKeys = [
ForeignKey(
entity = Task::class,
parentColumns = ["id"],
childColumns = ["taskId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Period (
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val taskId: Long,
val endDate: Date
)
TaskPeriod
package com.pochopsp.dailytasks.data.database.entity
import androidx.room.Embedded
import androidx.room.Relation
data class TaskPeriod (
@Embedded
val task: Task,
@Relation(
parentColumn = "id",
entityColumn = "taskId"
)
val period: Period
)
Completion
package com.pochopsp.dailytasks.data.database.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.Date
@Entity(foreignKeys = [
ForeignKey(
entity = Task::class,
parentColumns = ["id"],
childColumns = ["taskId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Completion (
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val taskId: Long,
val done: Boolean = false,
val date: Date
)
TaskViewModel
package com.pochopsp.dailytasks.domain.task
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pochopsp.dailytasks.data.database.Database
import com.pochopsp.dailytasks.data.database.dao.CompletionDao
import com.pochopsp.dailytasks.data.database.dao.DayDao
import com.pochopsp.dailytasks.data.database.dao.PeriodDao
import com.pochopsp.dailytasks.data.database.dao.TaskDao
import com.pochopsp.dailytasks.data.database.entity.Completion
import com.pochopsp.dailytasks.data.database.entity.Day
import com.pochopsp.dailytasks.data.database.entity.Period
import com.pochopsp.dailytasks.data.database.entity.Task
import com.pochopsp.dailytasks.domain.date.DateEvent
import com.pochopsp.dailytasks.domain.date.DatePeriod
import com.pochopsp.dailytasks.domain.date.DateWithoutTime
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.temporal.ChronoUnit
import java.util.Calendar
import java.util.Date
class TaskViewModel(
private val taskDao: TaskDao,
private val completionDao: CompletionDao,
private val dayDao: DayDao,
private val periodDao: PeriodDao
): ViewModel() {
val selectedDate = MutableStateFlow(Date())
val myDates = MutableStateFlow(DatePeriod(start = Date(), end = Date()))
init {
val calendar = initedCalendar()
val weekStart = calendar.time
calendar.add(Calendar.DAY_OF_MONTH, 6)
val weekEnd = calendar.time
myDates.value = DatePeriod(start = weekStart, end = weekEnd)
}
@OptIn(ExperimentalCoroutinesApi::class)
private val _dayTasks: StateFlow<List<TaskCardDto>> = myDates.flatMapLatest {
latestCurrentWeek ->
taskDao.getDayTasksForPeriod(
start = latestCurrentWeek.start,
end = latestCurrentWeek.end
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
// insert completions for period tasks who have not them in selected week then retrieve them
@OptIn(ExperimentalCoroutinesApi::class)
private val _periodTasks: StateFlow<List<TaskCardDto>> = myDates.flatMapLatest {
latestCurrentWeek -> MutableStateFlow(
taskDao.insertAndRetrieve(
weekStart = latestCurrentWeek.start,
weekEnd = latestCurrentWeek.end,
completionDao = completionDao
))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
private val _tasksInPeriod = MutableStateFlow(TasksInPeriod())
val tasksInPeriod: StateFlow<TasksInPeriod> = combine(_tasksInPeriod, _dayTasks, _periodTasks) { state, dayTasks, periodTasks ->
state.copy(
dateToTasks = taskDtosToMap(dayTasks + periodTasks),
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TasksInPeriod())
private val _createTasksState = MutableStateFlow(CreateTasksState())
val createTasksState = _createTasksState
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CreateTasksState())
private fun initedCalendar(): Calendar {
val calendar = Calendar.getInstance()
calendar.firstDayOfWeek = Calendar.MONDAY
calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
return calendar
}
private fun taskDtosToMap (taskDtos: List<TaskCardDto>): Map<DateWithoutTime,List<TaskCardDto>> {
val map = HashMap<DateWithoutTime,MutableList<TaskCardDto>>()
for (date in this.myDates.value.getDatesBetween()) {
map[DateWithoutTime(date)] = mutableListOf()
}
for(taskDto in taskDtos){
if (taskDto.date == null) continue
map[DateWithoutTime(taskDto.date)]?.add(taskDto)
}
return map
}
}