Таймер увеличивается на единицу всякий раз, когда пользователь пытается сбросить его до нуля в Android

Я создал стандартное приложение секундомера в 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
    }
}

но проблема все еще сохраняется.

0
0
52
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я решил эту проблему, проверив переменную 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 = false) без его сброса.

Leviathan 20.07.2024 13:12

На самом деле вы правы. У меня есть еще одна идея: что, если мы проверим isTimerRunning перед увеличением значения времени?

Alper Melkeli 20.07.2024 13:39
Ответ принят как подходящий

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, тогда происходит следующее:

  1. Поток ввода-вывода считывает time и увеличивает его на единицу, создавая 11. Это всего лишь результат расчета, который еще не записан обратно в переменную time.
  2. Основная ветка устанавливает от time до 0
  3. Поток ввода-вывода записывает результат вычисления обратно в time, который сейчас равен 11.

Таймер сейчас остановлен, но вместо сброса на 0 будет 11.

Это распространенная проблема в многопоточной среде при доступе к Общему изменяемому состоянию. В вашем конкретном случае это можно легко исправить, разрешив сопрограмме запускаться и в потоке пользовательского интерфейса, просто не используйте Dispatchers.IO. Это гарантирует, что одновременно может быть запущен только один блок кода, и ничто не сможет помешать, пока ваша сопрограмма в данный момент не ожидает delay.

На самом деле, вам вообще не следует использовать CoroutineScope, потому что это создает неуправляемую, висящую область действия сопрограммы, которую невозможно отменить. Если в компонуемом коде вы можете получить область видимости, вызвав rememberCoroutineScope(), то в модели представления вам следует использовать viewModelScope.


Одна из проблем при использовании метода активного увеличения счетчика time заключается в том, что ваши часы будут медленно отклоняться от прошедшего реального времени, потому что вы не просто увеличиваете счетчик ровно каждые 1000 миллисекунд, он всегда будет немного больше. См. первые два пункта, которые я сделал в https://stackoverflow.com/a/78742092/6216216 для подробного объяснения, почему это так.

Если вам не нужно, чтобы ваш секундомер был точным, это не будет проблемой, в противном случае вам нужно будет изменить свой подход, сохраняя только время, когда секундомер был запущен, как также объяснено (и подробно описано) в связанном ответ выше.

Спасибо, я просто переместил задержку в конец сопрограммы, и, похоже, пока она работает. Я до сих пор не понимаю, что вы сказали в другом вопросе о вычитании прошедшего времени из системного времени. Как узнать прошедшее время?

Bob Rasner 21.07.2024 00:54

Вместо запуска сопрограммы при запуске секундомера вы просто сохраняете текущее время в переменной. Допустим, время 11:22:33:. Затем каждую секунду вы обновляете свой пользовательский интерфейс, используя текущее время и вычитая из него время начала. После первой секунды это 11:22:34, поэтому вы рассчитываете 11:22:34 - 11:22:33 = 00:00:01. Еще через секунду будет 11:22:35, что приведет к 00:00:02 и так далее. Неважно, как часто вы обновляете свой пользовательский интерфейс (каждую секунду, каждую миллисекунду, каждый час), секундомер всегда будет точным.

Leviathan 21.07.2024 06:06

Вы даже можете сохранить время начала в базе данных, закрыть приложение, а когда приложение запустится снова, вы сможете возобновить отображение прошедшего времени. Он по-прежнему точен, потому что вы не подсчитываете прошедшие секунды, а просто рассчитываете прошедшее время. Вы можете найти код, необходимый для получения текущего времени и расчета прошедшего времени, в моем ответе на ваш предыдущий вопрос.

Leviathan 21.07.2024 06:06

Хорошо, сохраните LocalTime.now в переменную и вычтите из нее время начала. Каково время начала и как сохранить его в переменной?

Bob Rasner 21.07.2024 07:12

Как уже упоминалось, код для этого находится в моем ответе на ваш предыдущий вопрос. Вот еще раз ссылка: stackoverflow.com/a/78742092/6216216

Leviathan 21.07.2024 07:29

где мне поставить зависимость «org.jetbrains.kotlinx:kotlinx-datetime». В проекте или файле модуля? Попробовал оба, пишет "неразрешенная ссылка".

Bob Rasner 21.07.2024 07:50

Как и все зависимости, он принадлежит файлу градиента уровня модуля. Если у вас есть дополнительные вопросы, не стесняйтесь задавать их как новый вопрос, раздел комментариев предназначен только для обсуждения текущего вопроса/ответа (т. е. вашей resetTimer проблемы). Прежде чем спрашивать, обязательно выполните поиск по Stack Overflow, поскольку добавление зависимостей в файлы Gradle является очень распространенной задачей, и, скорее всего, уже будет вопрос, касающийся этого.

Leviathan 21.07.2024 08:05

Другие вопросы по теме