[Android] ๊ฐ„๋‹จํ•œ ์Šคํ†ฑ์›Œ์น˜ ์•ฑ ๋งŒ๋“ค๊ธฐ : Thread
๋ฐ˜์‘ํ˜•

 

 

 

 

Thread๋ฅผ ๋ณต์Šตํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ ์Šคํ†ฑ์›Œ์น˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์ œ์ž‘ํ–ˆ๋‹ค. (Joyce ์„œ์  ์˜ˆ์‹œ)

  • ๋ฉ”์ธ ์Šค๋ ˆ๋“œ
    - ์•ฑ์ด ์ฒ˜์Œ ์‹œ์ž‘๋  ๋•Œ ์ƒ์„ฑ๋˜๋Š” ์Šค๋ ˆ๋“œ
    - ์•กํ‹ฐ๋น„ํ‹ฐ์˜ ๋ชจ๋“  ์ƒ๋ช… ์ฃผ๊ธฐ ๊ด€๋ จ ์ฝœ๋ฐฑ ์‹คํ–‰์„ ๋‹ด๋‹น
    - ๋ฒ„ํŠผ, ์—๋”งํ…์ŠคํŠธ์™€ ๊ฐ™์€ UI ์œ„์ ฏ์„ ์‚ฌ์šฉํ•œ ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ์™€ UI ๋“œ๋กœ์ž‰ ์ด๋ฒคํŠธ ๋‹ด๋‹น
    - Handler ํด๋ž˜์Šค, AsyncTask ํด๋ž˜์Šค, runOnUiThread() ๋ฉ”์„œ๋“œ ๋“ฑ ํ™œ์šฉ
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ
    - ์ž‘์—…๋Ÿ‰์ด ํฐ ์—ฐ์‚ฐ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ, ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๋“ฑ
    - ์ ˆ๋Œ€๋กœ UI๊ด€๋ จ ์—ฐ์‚ฐ์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ํ•˜๋ฉด ์•ˆ ๋จ
    (๊ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ๊ฐ€ ์–ธ์ œ ์ฒ˜๋ฆฌ๋ฅผ ๋๋‚ด๊ณ  UI์— ์ ‘๊ทผํ•  ์ง€ ์•Œ ์ˆ˜ ์—†์Œ)
  • ๊ตฌ๊ธ€์—์„œ๋Š” ์•ฑ ๊ฐœ๋ฐœ ๋‹น์‹œ์˜ ์ตœ์‹  API๋ฅผ ํƒ€๊ฒŸ API๋กœ ์ž‘์„ฑํ•  ๊ฒƒ์„ ๊ถŒ๊ณ ํ•˜๊ณ  ์žˆ์Œ (๋ณด์•ˆ, ์„ฑ๋Šฅ ๋“ฑ)
  • ์‹œ๋ถ„์ดˆ ์ •๋ ฌ ์‹œ Baseline์„ ์ด์šฉํ•œ Constraint Layout ์ •๋ ฌ

 

๐Ÿ’ก timer(period = ){}

private fun start() { // ์Šคํ†ฑ์›Œ์น˜ ์ธก์ • ์‹œ์ž‘
        binding.btnStart.text = "์ผ์‹œ์ •์ง€"
        binding.btnStart.setBackgroundColor(getColor(R.color.red))
        isRunning = true // ์‹คํ–‰ ์ƒํƒœ ๋ณ€๊ฒฝ

        timer = timer(period = 10){ // ์Šคํ†ฑ์›Œ์น˜ ์‹œ์ž‘ ๋กœ์ง
            time++  // 10๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„ ํƒ€์ด๋จธ

            val millisecond = time % 100
            val second = (time % 6000) / 100
            val minute = time / 6000

            binding.tvMillisecond.text =
                if (millisecond < 10) ".0${millisecond}" else ".${millisecond}"
            binding.tvSecond.text =
                if (second < 10) ":0${second}" else ".${second}"
            binding.tvMinute.text = "${minute}"
        }
    }
  • ์ฝ”ํ‹€๋ฆฐ์—์„œ ์ œ๊ณตํ•˜๋Š” timer(period = [์ฃผ๊ธฐ]) {} ํ•จ์ˆ˜๋Š” ์ผ์ •ํ•œ ์ฃผ๊ธฐ๋กœ ๋ฐ˜๋ณตํ•˜๋Š” ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ž„
  • {} ์•ˆ์— ์“ฐ์ธ ์ฝ”๋“œ๋“ค์€ ๋ชจ๋‘ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰
  • ์ฃผ๊ธฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” period ๋ณ€์ˆ˜๋ฅผ 10์œผ๋กœ ์ง€์ •ํ–ˆ์œผ๋ฏ€๋กœ 10๋ฐ€๋ฆฌ์ดˆ๋งˆ๋‹ค ์‹คํ–‰
  • 0.01์ดˆ๋งˆ๋‹ค time์— 1์„ ๋”ํ•˜๋Š” ๊ฒƒ์€ ์ฃผ๊ธฐ๊ฐ€ 10๋ฐ€๋ฆฌ์ดˆ์ด๊ธฐ ๋•Œ๋ฌธ
  • ๋ทฐ์˜ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ƒ์„ฑํ•œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ๋งŒ ๊ทธ ๋ทฐ๋“ค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Œ 
    -> ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ UI์ž‘์—…์„ ํ•˜๋Š” ์ด ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ ์˜ค๋ฅ˜๊ฐ€ ๋œธ
  • ๋”ฐ๋ผ์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •

 

๐Ÿ’ก runOnUiThread

    private fun start() { // ์Šคํ†ฑ์›Œ์น˜ ์ธก์ • ์‹œ์ž‘
        binding.btnStart.text = "์ผ์‹œ์ •์ง€"
        binding.btnStart.setBackgroundColor(getColor(R.color.red))
        isRunning = true // ์‹คํ–‰ ์ƒํƒœ ๋ณ€๊ฒฝ

        timer = timer(period = 10) { // ์Šคํ†ฑ์›Œ์น˜ ์‹œ์ž‘ ๋กœ์ง
            time++  // 10๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„ ํƒ€์ด๋จธ

            val millisecond = time % 100
            val second = (time % 6000) / 100
            val minute = time / 6000

            runOnUiThread {  // UI ์Šค๋ ˆ๋“œ ์ƒ์„ฑ
                if (isRunning) // UI ์—…๋ฐ์ดํŠธ ์กฐ๊ฑด ์„ค์ •
                binding.tvMillisecond.text =
                    if (millisecond < 10) ".0${millisecond}" else ".${millisecond}"
                binding.tvSecond.text =
                    if (second < 10) ":0${second}" else ".${second}"
                binding.tvMinute.text = "${minute}"
            }
        }
    }
  • runonUiThread๋กœ UI ์Šค๋ ˆ๋“œ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๊ณ , ๊ทธ ์•ˆ์— ํ…์ŠคํŠธ๋ทฐ๋ฅผ ์„ค์ •ํ–ˆ๋˜ ์ฝ”๋“œ๋ฅผ ๋‹ค ๋„ฃ์–ด์ฃผ๋ฉด ์ •์ƒ ์ž‘๋™
  • isRunning์ด true์ผ ๋•Œ๋งŒ UI๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜์–ด์•ผ ํ•จ
    -> ์‚ฌ์šฉ์ž๊ฐ€ ํƒ€์ด๋จธ๋ฅผ ์ •์ง€ํ•˜๋Š” ์‹œ์ ๊ณผ UI์Šค๋ ˆ๋“œ์—์„œ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๋Š” ์‹œ์ ์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ

 

๐Ÿ’ก ์ตœ์ข… ๊ฒฐ๊ณผ ๋ฐ ์ฝ”๋“œ

๋”๋ณด๊ธฐ

activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_minute"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="45sp"
        app:layout_constraintBaseline_toBaselineOf="@+id/tv_second"
        app:layout_constraintEnd_toStartOf="@+id/tv_second"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/tv_second"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=":00"
        android:textSize="45sp"
        app:layout_constraintBottom_toTopOf="@+id/btn_refresh"
        app:layout_constraintEnd_toStartOf="@+id/tv_millisecond"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/tv_minute"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_millisecond"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=".00"
        android:textSize="30sp"
        app:layout_constraintBaseline_toBaselineOf="@+id/tv_second"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/tv_second" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_refresh"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_marginBottom="50dp"
        android:background="@color/yellow"
        android:padding="20dp"
        android:text="@string/refresh"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/btn_start"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_start"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_marginBottom="80dp"
        android:background="@color/blue"
        android:padding="20dp"
        android:text="@string/start"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

 

 

MainActivity.kt

package com.limheejin.stopwatch

import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.limheejin.stopwatch.databinding.ActivityMainBinding
import java.util.Timer
import kotlin.concurrent.timer

class MainActivity : AppCompatActivity(), View.OnClickListener { // ํด๋ฆญ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค

    private var isRunning = false // ์‹คํ–‰ ์—ฌ๋ถ€ ํ™•์ธ์šฉ ๋ณ€์ˆ˜
    private var timer: Timer? = null // timer ๋ณ€์ˆ˜ ์ถ”๊ฐ€
    private var time = 0 // time ๋ณ€์ˆ˜ ์ถ”๊ฐ€

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        enableEdgeToEdge()
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        initViews()
        setupListeners()

    }

    private fun initViews() {
        binding.btnStart.text = getString(R.string.start)
        binding.btnStart.setBackgroundColor(ContextCompat.getColor(this, R.color.blue))
        binding.tvMillisecond.text = ".00"
        binding.tvSecond.text = ":00"
        binding.tvMinute.text = "0"
    }

    private fun setupListeners() {
        binding.btnStart.setOnClickListener(this)
        binding.btnRefresh.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.btn_start -> {
                if (isRunning) {
                    pause()
                } else {
                    start()
                }
            }

            R.id.btn_refresh -> {
                refresh()
            }
        }
    }

    private fun start() { // ์Šคํ†ฑ์›Œ์น˜ ์ธก์ • ์‹œ์ž‘
        binding.btnStart.text = getString(R.string.pause)
        binding.btnStart.setBackgroundColor(getColor(R.color.red))
        isRunning = true // ์‹คํ–‰ ์ƒํƒœ ๋ณ€๊ฒฝ

        timer = timer(period = 10) { // ์Šคํ†ฑ์›Œ์น˜ ์‹œ์ž‘ ๋กœ์ง
            time++  // 10๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„ ํƒ€์ด๋จธ

            val millisecond = time % 100
            val second = (time % 6000) / 100
            val minute = time / 6000

            runOnUiThread {  // UI ์Šค๋ ˆ๋“œ ์ƒ์„ฑ
                if (isRunning) // UI ์—…๋ฐ์ดํŠธ ์กฐ๊ฑด ์„ค์ •
                binding.tvMillisecond.text = ".${millisecond.toString().padStart(2, '0')}"
                binding.tvSecond.text = ":${second.toString().padStart(2, '0')}"
                binding.tvMinute.text = minute.toString()
            }
        }
    }

    private fun pause() { // ์Šคํ†ฑ์›Œ์น˜ ์ผ์‹œ์ •์ง€
        isRunning = false // ๋ฉˆ์ถค์œผ๋กœ ์ „ํ™˜
        timer?.cancel() // ํƒ€์ด๋จธ ๋ฉˆ์ถ”๊ธฐ - cancel()ํ•จ์ˆ˜๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์— ์žˆ๋Š” ํ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๋น„์›€
        binding.btnStart.text = getString(R.string.start)
        binding.btnStart.setBackgroundColor(getColor(R.color.blue))
    }

    private fun refresh() { // ์Šคํ†ฑ์›Œ์น˜ ์ดˆ๊ธฐํ™”
        timer?.cancel() // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒ€์ด๋จธ ๋ฉˆ์ถ”๊ธฐ
        isRunning = false
        initViews()
        time = 0 // ํƒ€์ด๋จธ ์ดˆ๊ธฐํ™”
    }

//    override fun onDestroy() {
//        super.onDestroy()
//        timer?.cancel()
//    }

}

 

MainAcitivty.kt -> timer(period)๋ฅผ ์ด์šฉํ•œ ๊ฒƒ์ด ์•„๋‹Œ Thread ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ช…์‹œ์  ๊ตฌํ˜„

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private var isRunning = false
    private var time = 0

    private lateinit var binding: ActivityMainBinding
    private var timerThread: Thread? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        enableEdgeToEdge()
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        initViews()
        setupListeners()
    }

    private fun initViews() {
        // ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๋Š” ๋™์ผํ•˜๊ฒŒ ์œ ์ง€
    }

    private fun setupListeners() {
        binding.btnStart.setOnClickListener(this)
        binding.btnRefresh.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.btn_start -> {
                if (isRunning) {
                    pause()
                } else {
                    start()
                }
            }
            R.id.btn_refresh -> {
                refresh()
            }
        }
    }

    private fun start() {
        binding.btnStart.text = getString(R.string.pause)
        binding.btnStart.setBackgroundColor(getColor(R.color.red))
        isRunning = true

        timerThread = Thread {
            while (isRunning) {
                try {
                    Thread.sleep(10) // 10๋ฐ€๋ฆฌ์ดˆ๋งˆ๋‹ค ์ž‘์—… ์ˆ˜ํ–‰
                    time++

                    val millisecond = time % 100
                    val second = (time % 6000) / 100
                    val minute = time / 6000

                    runOnUiThread {
                        binding.tvMillisecond.text = ".${millisecond.toString().padStart(2, '0')}"
                        binding.tvSecond.text = ":${second.toString().padStart(2, '0')}"
                        binding.tvMinute.text = minute.toString()
                    }
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }
        }
        timerThread?.start()
    }

    private fun pause() {
        isRunning = false
        binding.btnStart.text = getString(R.string.start)
        binding.btnStart.setBackgroundColor(getColor(R.color.blue))
    }

    private fun refresh() {
        isRunning = false
        timerThread?.interrupt() // ์Šค๋ ˆ๋“œ๋ฅผ ์ค‘์ง€ํ•˜๊ธฐ ์œ„ํ•ด interrupt ํ˜ธ์ถœ
        initViews()
        time = 0
    }

    override fun onDestroy() {
        super.onDestroy()
        timerThread?.interrupt() // ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ข…๋ฃŒ๋  ๋•Œ ์Šค๋ ˆ๋“œ๋ฅผ ์ค‘์ง€
    }
}

 

 

 

 

 

๋ฐ˜์‘ํ˜•
 ๐Ÿ’ฌ C O M M E N T