Я создал стандартное приложение секундомера в Android Studio, используя компоновку реактивного ранца. Это дает пользователю возможность сбросить секундомер обратно на ноль, однако есть проблема. Секундомер вернется к нулю, но затем сразу же увеличится на единицу.
Вот код.
Существует переменная для хранения времени, называемая «время». Существует переменная, определяющая, работает ли таймер, под названием «isTimerRunning». Существует функция, которая запускает таймер с использованием цикла while внутри области сопрограммы, называемая «startTimer». Существует функция, которая останавливает таймер и сбрасывает его обратно в ноль, называемая «resetTimer». Существует компонуемый текст, который отображает время. Там две кнопки, одна запускает таймер, другая его сбрасывает.
// Time Variable
var time by remember { mutableIntStateOf(0) }
// Is Timer Running Variable
var isTimerRunning by remember { mutableStateOf(false) }
// Start Timer Function
fun startTimer() {
isTimerRunning = true
CoroutineScope(Dispatchers.IO).launch {
while (isTimerRunning) {
delay(1000)
time += 1
}
}
}
// Reset Timer Function
fun resetTimer() {
isTimerRunning = false
time = 0
}
// App Layout
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
// Display Timer
Text(text = time.toString())
// Start Timer Button
Button(onClick = { startTimer() }) {
Text(text = "Start")
}
// Reset Timer Button
Button(onClick = { resetTimer() }) {
Text(text = "Reset")
}
}
Как я уже говорил выше, таймер увеличивается на единицу после сброса на ноль. Я попытался поместить сопрограмму в функцию «resetTimer» и установить задержку в тридцать миллисекунд между двумя переменными, вот так:
fun resetTimer() {
CoroutineScope(Dispatchers.IO).launch {
isTimerRunning = false
delay(30)
time = 0
}
}
но проблема все еще сохраняется.
Я решил эту проблему, проверив переменную isTimerRunning внутри цикла while функции startTimer перед увеличением значения времени. Насколько я понимаю, область сопрограммы не может проверить, является ли isTimerRunning ложным, если он запускает метод задержки в цикле while, поэтому нам нужно проверить его и внутри.
// Start Timer Function
fun startTimer() {
isTimerRunning = true
CoroutineScope(Dispatchers.IO).launch {
while (isTimerRunning) {
delay(1000)
if (isTimerRunning) time += 1
}
}
}
Спасибо за отчет Левиафан.
На самом деле вы правы. У меня есть еще одна идея: что, если мы проверим isTimerRunning перед увеличением значения времени?
resetTimer
останавливает таймер, установив isTimerRunning = false
. Таймер в основном просто ждет delay(1000)
, поэтому, когда вы установите isTimerRunning = false
, он, вероятно, будет делать именно это в данный момент. После завершения сделки timer
увеличивается на единицу (нежелательное поведение, которое вы наблюдали), и только затем в цикле while проверяется, истинно ли isTimerRunning
. Это не так, поэтому цикл завершается и сопрограмма завершается, но таймер уже равен 1, а не 0.
Вы можете исправить это, изменив свою сопрограмму на это:
delay(1000)
while (isTimerRunning) {
time += 1
delay(1000)
}
Теперь следующее, что происходит, когда delay
выполнено, — это проверка истинности isTimerRunning
, а не увеличение таймера.
Хотя в большинстве случаев это будет работать, на самом деле это состояние гонки, поскольку у вас задействованы два отдельных потока, которые обращаются к одному и тому же изменяемому состоянию: может случиться так, что сопрограмма уже проверила, что isTimerRunning
истинно, тогда resetTimer
устанавливает isTimerRunning = false
и time
на 0, а затем сопрограмма устанавливает time += 1
, в результате чего таймер сброса снова становится 1
.
Хуже того, последнее утверждение на самом деле представляет собой два разных утверждения:
time = time + 1
Первый читает time
и увеличивает его на 1, второй устанавливает результат этого вычисления в time
. Теперь представьте, что между этими двумя потоками другой работающий поток resetTimer
сбрасывает time
в 0. Предположим, что время было 10
раньше, и пусть два задействованных потока называются Main и IO, тогда происходит следующее:
time
и увеличивает его на единицу, создавая 11
. Это всего лишь результат расчета, который еще не записан обратно в переменную time
.time
до 0
time
, который сейчас равен 11
.Таймер сейчас остановлен, но вместо сброса на 0
будет 11
.
Это распространенная проблема в многопоточной среде при доступе к Общему изменяемому состоянию. В вашем конкретном случае это можно легко исправить, разрешив сопрограмме запускаться и в потоке пользовательского интерфейса, просто не используйте Dispatchers.IO
. Это гарантирует, что одновременно может быть запущен только один блок кода, и ничто не сможет помешать, пока ваша сопрограмма в данный момент не ожидает delay
.
На самом деле, вам вообще не следует использовать CoroutineScope
, потому что это создает неуправляемую, висящую область действия сопрограммы, которую невозможно отменить. Если в компонуемом коде вы можете получить область видимости, вызвав rememberCoroutineScope()
, то в модели представления вам следует использовать viewModelScope
.
Одна из проблем при использовании метода активного увеличения счетчика time
заключается в том, что ваши часы будут медленно отклоняться от прошедшего реального времени, потому что вы не просто увеличиваете счетчик ровно каждые 1000 миллисекунд, он всегда будет немного больше. См. первые два пункта, которые я сделал в https://stackoverflow.com/a/78742092/6216216 для подробного объяснения, почему это так.
Если вам не нужно, чтобы ваш секундомер был точным, это не будет проблемой, в противном случае вам нужно будет изменить свой подход, сохраняя только время, когда секундомер был запущен, как также объяснено (и подробно описано) в связанном ответ выше.
Спасибо, я просто переместил задержку в конец сопрограммы, и, похоже, пока она работает. Я до сих пор не понимаю, что вы сказали в другом вопросе о вычитании прошедшего времени из системного времени. Как узнать прошедшее время?
Вместо запуска сопрограммы при запуске секундомера вы просто сохраняете текущее время в переменной. Допустим, время 11:22:33:. Затем каждую секунду вы обновляете свой пользовательский интерфейс, используя текущее время и вычитая из него время начала. После первой секунды это 11:22:34, поэтому вы рассчитываете 11:22:34 - 11:22:33 = 00:00:01. Еще через секунду будет 11:22:35, что приведет к 00:00:02 и так далее. Неважно, как часто вы обновляете свой пользовательский интерфейс (каждую секунду, каждую миллисекунду, каждый час), секундомер всегда будет точным.
Вы даже можете сохранить время начала в базе данных, закрыть приложение, а когда приложение запустится снова, вы сможете возобновить отображение прошедшего времени. Он по-прежнему точен, потому что вы не подсчитываете прошедшие секунды, а просто рассчитываете прошедшее время. Вы можете найти код, необходимый для получения текущего времени и расчета прошедшего времени, в моем ответе на ваш предыдущий вопрос.
Хорошо, сохраните LocalTime.now в переменную и вычтите из нее время начала. Каково время начала и как сохранить его в переменной?
Как уже упоминалось, код для этого находится в моем ответе на ваш предыдущий вопрос. Вот еще раз ссылка: stackoverflow.com/a/78742092/6216216
где мне поставить зависимость «org.jetbrains.kotlinx:kotlinx-datetime». В проекте или файле модуля? Попробовал оба, пишет "неразрешенная ссылка".
Как и все зависимости, он принадлежит файлу градиента уровня модуля. Если у вас есть дополнительные вопросы, не стесняйтесь задавать их как новый вопрос, раздел комментариев предназначен только для обсуждения текущего вопроса/ответа (т. е. вашей resetTimer
проблемы). Прежде чем спрашивать, обязательно выполните поиск по Stack Overflow, поскольку добавление зависимостей в файлы Gradle является очень распространенной задачей, и, скорее всего, уже будет вопрос, касающийся этого.
Однако это не сработает, если будет еще одна функция, которая просто приостанавливает таймер (также устанавливая
isTimerRunning = false
) без его сброса.