스톱워치

이번 포스팅에서는 코틀린에서 스톱워치 기능을 구현하는 2가지 방법을 소개한다.

  1. LocalDateTime을 이용하는 방법 -> 오류 발생
  2. TimeUnit을 이용하는 방법 -> 훨씬 간결하다, 추천


currentTimeMillis()

System.currentTimeMillis()을 이용하면 현재 시간을 Long타입으로 변환해서 알려준다.

viewModelScope.launch {
    lastTimestamp = System.currentTimeMillis()          // 1초 전 시각
    delay(1000)
    timeMillis += System.currentTimeMillis() - lastTimestamp // 현재 시각
}

현재 시각 - 1초 전 시각 = 1초 가 나온다.
1초를 계속 더하면 StopWatch 기능을 구현할 수 있다고 생각했다!! 위와 같은 방법에는 기가막힌 오류가 있었다. 오류는 나중에 살펴보고 간단한 코드를 설명해보겠다.

  • 전체 코드

      @RequiresApi(Build.VERSION_CODES.O)
      fun startTime() {
          job3 = viewModelScope.launch {
              lastTimestamp = System.currentTimeMillis()
              while (recoding.value) {
                  delay(1000)
                  timeMillis += System.currentTimeMillis() - lastTimestamp
                  Log.d("daeYoung", "currentTimeMillis: ${formatTime(System.currentTimeMillis())}")
                  Log.d("daeYoung", "lastTimestamp: ${lastTimestamp}")
                  lastTimestamp = System.currentTimeMillis()
                  _formattedTime.value = formatTime(timeMillis)
                  Log.d("daeYoung", "time: ${_formattedTime.value}")
              }
          }
      }
    
      @RequiresApi(Build.VERSION_CODES.O)
      private fun formatTime(timeMillis: Long): String {
      //        val localDateTime = LocalDateTime.ofInstant(
      //            Instant.ofEpochMilli(timeMillis),
      //            ZoneId.systemDefault()
      //        )
          val localDateTime = Instant.ofEpochMilli(timeMillis).atZone(ZoneId.systemDefault()).toLocalDateTime()
          val formatter = DateTimeFormatter.ofPattern(
              "HH : mm : ss",
              Locale.getDefault()
          )
          return localDateTime.format(formatter)
      }
    

    LocalDate, LocalDateTime 클래스는 Android 버전 코드 오레오(Build.VERSION_CODES.O) / API level 26 이상부터 지원이 됩니다.

    당연히 우리의 어플은 minSdk가 26 미만일 것이다. 그래서, 하위 버전에서도 위의 클래스를 지원하도록 해줘야 한다. 그래서 @RequiresApi(Build.VERSION_CODES.O)을 추가한다.

    만약 @RequiresApi(Build.VERSION_CODES.O)을 추가하는 방법이 귀찮다면 gradle(모듈수준)에 dependency를 추가해주면 된다.

    android {
      ...
      compileOptions {
          coreLibraryDesugaringEnabled true   // LocalDateTime 이 Api 26 이하 지원을 위해서 추가함.
          ...
      }
    }
    
    dependencies {
        coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
        ...
    }
    

milliseconds -> LocalDateTime

milliseconds를 원하는 format으로 변환하기 위해서는 먼저 LocalDateTime을 바꿔줘야한다.
ex) milliseconds를 ““HH : mm : ss : SSS”로 바꿀 때 사용

private var timeMillis = 0L
//        val localDateTime = LocalDateTime.ofInstant(
//            Instant.ofEpochMilli(timeMillis),
//            ZoneId.systemDefault()
//        )
        val localDateTime = Instant.ofEpochMilli(timeMillis).atZone(ZoneId.systemDefault()).toLocalDateTime()

localDateTime으로 바꿔주는 코드는 2개가 있고(주석과, 주석아래의 코드) 입맛에 맞는것으로 골라 사용하면된다.

LocalDateTime -> 원하는 포멧

LocalDateTime을 ““HH : mm : ss”로 바꿔보자.

val formatter = DateTimeFormatter.ofPattern(
    "HH : mm : ss",
    Locale.getDefault()
)
return localDateTime.format(formatter)

StopWatch 코드 구현(문제 발생)

현재시각 - 1초 전 시각 = 1초 을 활용해서 1초마다 증가시키는 코드를 만들어 StopWatch를 구현하자.

private var timeMillis = 0L       // 현재시각(밀리초)
private var lastTimestamp = 0L    // 1초전 시각(밀리초)

fun startTime1() {
    job3 = viewModelScope.launch {
        lastTimestamp = System.currentTimeMillis()
        while (true) {   // 계속 1초씩 증가하는 무한루프
            delay(1000)
            timeMillis += System.currentTimeMillis() - lastTimestamp
            lastTimestamp = System.currentTimeMillis()
            _formattedTime.value = formatTime1(timeMillis)
        }
    }
}

private fun formatTime1(timeMillis: Long): String {
    val localDateTime = Instant.ofEpochMilli(timeMillis).atZone(ZoneId.systemDefault()).toLocalDateTime()
    val formatter = DateTimeFormatter.ofPattern(
        "HH : mm : ss",
        Locale.getDefault()
    )
    return localDateTime.format(formatter)
}

위의 코드만 봤을 때 문제 없이 잘 돌아가야한다. 그러나 문제가 생겼다.
현재시각 - 1초 전 시각 을 “HH : mm : ss”로 바꿨을 때 “00 : 00 : 01”로 시작해야한다. 그러나 아래의 영상과 같이 “09 : 00 : 01” 로 9시부터 보여진다.


TimeUnit 이용(해결법)

TimeUnit을 이용한 코드는 위의 방법보다 훨씬 간단하다.

  • TimeUnit.MILLISECONDS.toHours(milliseconds) % 24 밀리초를 시간으로 바꿔주고 24보다 숫자가 작게 나온다.
  • TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60 밀리초를 분으로 바꿔주고 60보다 숫자가 작게 나온다.
  • TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 밀리초를 초로 바꿔주고 60보다 숫자가 작게 나온다.
 // 시간 측정
    @RequiresApi(Build.VERSION_CODES.O)
    fun startTime() {
        job3 = viewModelScope.launch {
            while (true) {
                delay(1000)
                milliseconds += 1000L
                _formattedTime.value = formatTime(milliseconds)
            }
        }
    }

    // 시간 format 설정
    @RequiresApi(Build.VERSION_CODES.O)
    private fun formatTime(milliseconds: Long): String {
        val hours = TimeUnit.MILLISECONDS.toHours(milliseconds) % 24
        val minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60
        val seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60
        val formatter = String.format("%02d : %02d : %02d", hours, minutes, seconds)
        return formatter
    }

위의 코드로 실행하면 처음에 본 영상과 같이 성공적으로 stopwatch 기능을 구현할 수 있다.

References

milliseconds -> LocalDateTime
LocalDateTime -> 원하는 포멧
Compose를 활용하여 스탑워치 데스크탑앱 만들기 TimeUnit 이용