I have a composable which uses Canvas to draw tasks on a clock-like dial. The tasks are read from a ViewModel state, which in turn loads them from a database. The list of tasks varies depending on the selected date. When you choose a date (e.g. 26th June) on a calendar component, the ViewModel loads the tasks for 26th June and draws them (task drawing not shown in the example below).
When I tap the Canvas, the log shows data for the 26th June. If I select a new date (e.g. 27th June) the tasks for the 27th June are loaded properly.
However, when I tap the dial on a 27th June, the data from the 26th June (i.e. the previous date) is loaded again for some reason behind the scenes. The Logcat logs show 26th June data (initial loading of data), 26th June data (after tapping the Canvas), 27th June data (after selecting the 27th June), and then again 26th June data (after tapping the Canvas) even though I haven't selected this date again - the dial was definitely drawn with the correct data though (for 27th June).
The fact that the data goes back to the 26th June is a problem. I expect it to stay on the 27th June until I select a different date. This going-back-of-data-to-previous-date problem happens only when I tap the dial and then select a different date. If I keep selecting different dates without tapping the dial in between date selection, the data doesn't go back to previous date on its own.
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import kotlin.math.min
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import com.example.circularplanner.ui.viewmodel.DayState
import com.example.circularplanner.ui.viewmodel.TaskUiState
import com.example.circularplanner.ui.viewmodel.UserInput
import com.example.circularplanner.utils.TaskDialUtils.square
import java.time.LocalDate
import kotlin.math.sqrt
@Composable
fun TestTaskDial(
dayState: DayState,
userInput: UserInput,
onSetDate: (LocalDate) -> Unit
) {
// The width and height of the Canvas
var width by remember { mutableStateOf(0) }
var height by remember { mutableStateOf(0) }
var touchInsideTheDial by remember { mutableStateOf(false) }
var center by remember { mutableStateOf(Offset.Zero) }
// Basically the width of the finger touch on the screen
var touchStroke: Float = 50f
// The radius of the dial (from the center to the end of the clock steps)
var outerRadius by remember { mutableStateOf(0f) }
Log.i("DDselectedDate", userInput.selectedDate.toString())
Log.i("DDdate", dayState.date.toString())
Log.i("DDtasks", dayState.tasks.joinToString())
fun distance(first: Offset, second: Offset): Float {
return sqrt((first.x - second.x).square() + (first.y - second.y).square())
}
fun checkIfTouchInsideDial(distance: Float, radius: Float, touchStroke: Float): Boolean {
if (distance <= radius + touchStroke * 2f) {
return true
} else {
return false
}
}
Box(
modifier = Modifier
.fillMaxSize()
) {
Canvas (
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.8f)
.onGloballyPositioned {
width = it.size.width
height = it.size.height
center = Offset(width / 2f, height / 2f)
// The radius of the dial
outerRadius = min(width.toFloat(), height.toFloat()) / 2f
}
.pointerInput(Unit) {
detectTapGestures(
onTap = { offset ->
val distance = distance(offset, center)
touchInsideTheDial = checkIfTouchInsideDial(
distance,
outerRadius,
touchStroke
)
if (touchInsideTheDial) {
Log.i("TaskDial", "onTap")
Log.i("DDselectedDate", userInput.selectedDate.toString())
Log.i("DDdate", dayState.date.toString())
Log.i("DDtasks", dayState.tasks.joinToString())
}
}
)
}
) {
outerRadius = min(width, height) / 2f * 0.8f
// Draw dial
drawCircle(
color = Color.Cyan,
center = center,
radius = outerRadius,
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(onClick = { onSetDate(LocalDate.parse("2025-06-26")) }) {
Text("26th June")
}
Button(onClick = { onSetDate(LocalDate.parse("2025-06-27")) }) {
Text("27th June")
}
}
Row (
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.Center
) {
Text(text = userInput.selectedDate.toString())
}
}
}
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.example.circularplanner.data.Task
import com.example.circularplanner.data.Time
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import java.time.LocalDate
import java.util.UUID
import kotlin.String
typealias Tasks = List<Task>
data class DayState (
val date: LocalDate = LocalDate.now(),
val activeTimeStart: Time = Time(6, 0),
val activeTimeEnd: Time = Time(22, 0),
val tasks: Tasks = emptyList()
)
data class UserInput(
val selectedDate: LocalDate = LocalDate.parse("2025-06-26"),
)
data class Day(
val date: LocalDate,
val activeTimeStart: Time,
val activeTimeEnd: Time,
)
data class Time(
var hour: Int = 0,
var minute: Int = 0
)
data class Task (
val date: LocalDate,
var title: String,
var startTime: Time,
var endTime: Time,
var description: String = "",
val id: UUID = UUID.randomUUID()
)
class TestViewModel() : ViewModel() {
companion object {
private const val MILLS = 5_000L
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
return TestViewModel() as T
}
}
}
val days = mapOf(
LocalDate.parse("2025-06-26") to Day(
date = LocalDate.parse("2025-06-26"),
activeTimeStart = Time(6,0),
activeTimeEnd = Time(22,0)
),
LocalDate.parse("2025-06-27") to Day(
date = LocalDate.parse("2025-06-27"),
activeTimeStart = Time(6,0),
activeTimeEnd = Time(20,30)
)
)
val tasks = mapOf(
LocalDate.parse("2025-06-26") to listOf(
Task(
date = LocalDate.parse("2025-06-26"),
title = "Bis Dahin",
startTime = Time(11,3),
endTime = Time(13,0),
description = "Willkommen bei Gboard! Texte, die Sie kopieren, werden hier gespeichert.",
id = UUID.fromString("b8d9b09b-d18a-4e00-a347-9d388b5c779a")
),
Task(
date = LocalDate.parse("2025-06-26"),
title = "No Let's Go",
startTime = Time(14,56),
endTime = Time(16,55),
description = "Programming Android with Kotlin",
id = UUID.fromString("c68ee93c-7100-4020-9006-c48b8402488d")
)
),
LocalDate.parse("2025-06-27") to listOf(
Task(
date = LocalDate.parse("2025-06-27"),
title = "Hallo Herr Schmidt",
startTime = Time(15,34),
endTime = Time(20,0),
description = "Willkommen bei Gboard! Texte, die Sie kopieren, werden hier gespeichert.",
id = UUID.fromString("14873472-8bb4-43a8-9f5d-a0265a5f8eda")
)
)
)
val userInput = MutableStateFlow(UserInput())
@OptIn(ExperimentalCoroutinesApi::class)
val dayState = userInput.flatMapLatest { input: UserInput ->
Log.i("TaskViewModel", "loading dayState")
val dayFlow = flow {
emit(days.get(input.selectedDate))
}
val tasksFlow = flow {
emit(tasks.get(input.selectedDate))
}
combine(
flow = dayFlow,
flow2 = tasksFlow
) { day, tasks ->
DayState(
date = input.selectedDate,
activeTimeStart = day?.activeTimeStart ?: Time(6, 0),
activeTimeEnd = day?.activeTimeEnd ?: Time(22, 0),
tasks = tasks!!
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
initialValue = DayState()
)
fun setSelectedDate(date: LocalDate) {
Log.i("setSelectedDate", date.toString())
userInput.update {
it.copy(
selectedDate = date
)
}
}
}