Insert then retrieve data Room and Jetpack Compose Flow

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 Flows. 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
    }
    
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật