오늘은 타이머 앱을 개발하면서 스킬셋 학습을 진행해 볼 것입니다.
1) 카운트다운
- AlertDialog.Builder를 사용하며 커스텀뷰로 팝업에 심어서 시작할 때 스톱워치가 진행되도록 합니다.
2) FloatingButton의 변화
- 시작 버튼을 누르면 일시정지 버튼으로 보이고 정지 버튼은 구간측정 버튼으로 바뀝니다.
- 일시정지를 누르면 다시 정지버튼과 재생버튼이 활성화됩니다.
3) 워커스레드 구현
- 시간이 흐르는 것은 Worker Thread에서 구현되고 UI 변경 부분은 UI Thread에서 구현되도록 합니다.
4) 구간측정
- 구간 측정된 데이터는 뷰그룹에 addView 합니다.
다음 내용에 대해서 차근히 알아볼 것입니다.
우선 AlertDialog.Builder를 통해 팝업을 만드는데, 팝업 내 UI를 구성하기 위해서 커스텀으로 xml을 만들어
적용하는 방법입니다.
1) 카운트다운 구현
저는 dialog_countdown_setting.xml로 리소스 파일을 레이아웃 폴더에 하나 만들었습니다.
이 안에서 NumberPicker를 사용해서 시간초를 선택할 수 있도록 하겠습니다.
[dialog_countdown_setting.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:layout_width="match_parent"
android:layout_height="match_parent">
<NumberPicker
android:id="@+id/countdownSecondPicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
<TextView
android:id="@+id/countdonwUnitTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="초"
app:layout_constraintBottom_toBottomOf="@+id/countdownSecondPicker"
app:layout_constraintStart_toEndOf="@+id/countdownSecondPicker"
app:layout_constraintTop_toTopOf="@+id/countdownSecondPicker" />
</androidx.constraintlayout.widget.ConstraintLayout>
이렇게 한 가운데에 NumberPicker를 두고 TextView는 '초'를 쓰기 위해서 두었습니다.
이제 이걸 AlertDialog.Builder를 생성해서 호출할 때, binding을 생성한 다음 setView를 통해
팝업 내 커스텀 뷰로 심도록 할 것입니다.
private fun showCountdownSettingDialog(){
AlertDialog.Builder(this).apply {
val dialogBinding = DialogCountdownSettingBinding.inflate(layoutInflater)
setView(dialogBinding.root)
// 반환 값이 없는 상태에서 객체에 속성을 정의하는 것
// NumberPicker 접근 -> 속성 정리
with(dialogBinding.countdownSecondPicker){
maxValue = 20
minValue = 0
value = countdownSecond // 카운트다운된 초 -> NumberPicker의 값으로 셋팅
}
setTitle("카운트 다운 설정")
setPositiveButton("확인") { _, _ ->
countdownSecond = dialogBinding.countdownSecondPicker.value
currentCountDownDeciSecond = countdownSecond * 10
binding.countdownTextView.text = String.format("%02d", countdownSecond)
}
setNagativeButton("취소", null)
}.show()
}
이렇게 AlertDialog.Builder를 현재 컨텍스트에서 만들되, 객체 내 속성 적용 함수인 apply 확장함수를 이용해서
커스텀 뷰를 바인딩합니다. 인플레이팅하고 setView에 꽂아주면 끝! 그대로 위젯 접근도 다 가능하게
쉽게 구현이 가능합니다.
private fun showAlertDialog(){
AlertDialog.Builder(this).apply {
setMessage("종료하시겠습니까?")
setPositiveButton("네") { _, _ ->
stop()
}
setNegativeButton("아니오", null)
}.show()
}
위는 종료 팝업이며, AlertDialog는 필히 show를 붙여서 정의해주시길 바랍니다.
2. FloatingButton 구현
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/startButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/playColor"
android:clickable="true"
app:flow_verticalBias="1"
app:layout_constraintBottom_toBottomOf="@+id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline"
app:srcCompat="@android:drawable/ic_media_play" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/pauseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/pauseColor"
android:clickable="true"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline"
app:srcCompat="@android:drawable/ic_media_pause" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/stopButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/stopColor"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline"
app:srcCompat="@android:drawable/alert_light_frame" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lapButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/lapColor"
android:clickable="true"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline"
app:srcCompat="@android:drawable/star_big_on" />
플로팅 버튼은 시작과 일시정지, 정지와 구간측정 버튼을 동일한 위치에 두고 일시정지와 구간측정은
보이지 않도록 구현해둡니다.
이는 시작 버튼을 누르고 나서 보이게 할 것이며 색상 또한 변경해서 구분되어 보여지도록 할 것입니다.
구간 측정할 때 시간이 레이아웃에 입력되도록 하고자 스크롤 뷰 내 LinearLayout을 두고
이 안에 텍스트뷰를 넣는 방법으로 진행해보겠습니다.
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="50dp"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline">
<LinearLayout
android:id="@+id/lapContainerLinearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
<androidx.constraintlayout.widget.Group
android:id="@+id/countdownGroup"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="countdownTitleTextView, countdownTextView,countdownProgressBar, countdonwUnitTextView" />
이렇게 셋팅해두고 스크롤 뷰 내에 뷰그룹 하나를 더 두어 추후에 텍스트뷰 생성해서 구간 측정 값을
넣고 그대로 addView 하는 형태로 구현할 예정입니다.
또한 위젯끼리 선택해두고 우클릭해서 Helpers를 통해 그룹으로 추가하게되면
constraint_referenced_ids에 위젯 아이디를 넣고 이 위젯들은 하나의 그룹으로 정의할 수 있습니다.
이렇게 설정한 이유는 카운트다운이 다 종료되면 카운트 다운으로 표기된 부분을 전체적으로
안보이도록 구현하고자 그룹화 한 것 입니다.
3. 워커 스레드 구현
우선 플로팅 버튼 클릭시 시작/일시정지/정지/구간측정 이 4가지 기능을 구현해보겠습니다.
이는 UI에 직접 접근하는 것이기 때문에 UI Thread 그대로 사용합니다.
binding.countdownTextView.setOnClickListener {
showCountdownSettingDialog()
}
binding.startButton.setOnClickListener {
start()
binding.startButton.isVisible = false
binding.stopButton.isVisible = false
binding.pauseButton.isVisible = true
binding.lapButton.isVisbile = true
}
binding.stopButton.setOnClickListener {
showAlertDialog()
}
binding.pauseButton.setOnClickListener {
pause()
binding.startButton.isVisible = true
binding.stopButton.isVisible = true
binding.pauseButton.isVisible = false
binding.lapButton.isVisible = false
}
binding.lapButton.setOnClickListener {
lap()
}
이렇게 각 플로팅 버튼에 리스너를 달아준 다음에 앞서 설명한 대로 시작하고 있을때 일시정지와 구간측정 버튼이
보여지게 되고 정지된 상태에서는 재상버튼과 정지버튼이 보여지도록 합니다.
여기서 시간이 측정되는 부분을 구현할 건데 이는 워커 스레드에서 구현되어야 하고 UI 반영은 UI 스레드에
Runnable 객체를 넘기거나 Message를 넘김으로써 가능합니다.
방법은 아래와 같습니다.
1) runOnUiThread를 사용하여 워커 스레드에서 구현된 내용을 UI 스레드에 적용한다.
2) View.Post { }를 통해 워커 스레드에서 구현된 내용을 UI 스레드에 적용한다.
3) Handler를 통해 메시지나 Runnable 객체를 보낸다.
- Handler는 UI Thread에서 정의하고 Runnable 객체 안에서는 워커스레드에서 작업하는 것을 구현한다.
- Runnable 객체가 모두 정의가 되면 UI Thread에서 Handler를 통해 Post로 보낸다.
1) runOnUiThread & view.post 로 구현하는 방법
private var timerThread: Timer? = null
private fun start(){
timerThread = Timer(initialDelay = 0, period = 100){ // 100ms
// 카운트다운 밀리초가 0이 된다면
// 타이머 밀리초를 늘려서 보여지도록 한다.
if(currentCountDownDeciSecond == 0){
currentDeciSecond += 1
val minutes = currentDeciSecond.div(10) / 60 // (타이머 밀리초를 10으로 나누고 60으로 나눈 값 = 분)
val seconds = currentDeciSecond.div(10) % 60 // (타이머 밀리초를 10으로 나누고 60으로 나눈 나머지 값 = 초)
val deciSeconds = currentDeciSecond % 10
// 값 구현한 내용은 워커스레드지만 UI 변경사항 적용은 UIThread에서! (첫 번쨰 방법)
runOnUiThread {
binding.timeTextView.text = String.format("%02d:%02d", minutes, seconds)
binding.tickTextView.text = deciSeconds.toString()
binding.countdownGroup.isVisble = false
}
}
// 카운트다운 밀리초가 0이 아니라면 해당 밀리초는 하나씩 줄어야함
else {
currentCountDownDeciSecond -= 1
val seconds = currentCountDownDeciSecond / 10
val progress = (currentCountDownDeciSecond / (countdownSecond * 10f)) * 100
// UI 변경은 UI Thread에서 하도록 view.post(두 번째 방법)
binding.root.post {
binding.countdownTextView.text = String.format("%02d", seconds)
binding.countdownProgressBar.progress = progress.toInt()
}
}
}
}
2) Handler로 구현하는 방법
// Looper는 UI Thread에서 Message Queue를 지속해서 모니터링합니다.
// 모니터링 하다가 메시지나 Runnable 객체가 있다면 처리합니다. (UI Thread로 보냄)
private val handler = Handler(Looper.getMainLooper())
private lateinit var timeRunnable: Runnable
var isRunning = false // 타이머 상태
private fun start(){
timeRunnable = object: Runnable {
// 워커 스레드에서 구현되는 내용입니다.
override fun run(){
if(!isRunning) return
if (currentCountDownDeciSecond == 0) {
currentDeciSecond += 1
val minutes = currentDeciSecond.div(10) / 60
val seconds = currentDeciSecond.div(10) % 60
val deciSeconds = currentDeciSecond % 10
runOnUiThread {
binding.timeTextView.text = String.format("%02d:%02d", minutes, seconds)
binding.tickTextView.text = deciSeconds.toString()
binding.countdownGroup.isVisible = false
}
}
else {
currentCountDownDeciSecond -= 1
val seconds = currentCountDownDeciSecond / 10
val progress =
(currentCountDownDeciSecond / (countdownSecond * 10f)) * 100
binding.root.post {
binding.countdownTextView.text = String.format("%02d", seconds)
binding.countdownProgressBar.progress = progress.toInt()
}
}
// Runnalbe 객체 안에서 handler로 작업처리 요청하는데, 100밀리초 지연시간해서 작업처리
handler.postDelayed(this, 100) // 100ms
}
}
// UI Thread에 보냄
handler.post(timeRunnable)
}
이렇게 Worker Thread 안에서 (Runnable 객체 구현) Handler.postDelayed를 이용해서 현 runnable 객체를
100ms마다 워커스레드에서 작동하도록 정의합니다.
이후에 handler.post(runnable 객체)를 통해서 UI 업데이트를 수행합니다.
이후에 정지할 때에는 runOnUiThread나 view post를 통해서 보낸 경우라면,
스레드를 cancel() 해서 워커 스레드를 종료하는 방법이 있고 Handler를 통해서 워커스레드를
구동했다면, removeCallbacks()을 통해서 Runnable 객체(-> 워커스레드)를 종료하면 됩니다.
전체 코드는 마지막에 참조하고 Git 주소도 남기도록 하겠습니다.
4. 구간 측정
구간 측정은 TextView 객체를 코틀린에서 생성한 다음에 Layout 뷰그룹에 addView 하는 형태로 진행하겠습니다.
private fun lap(){
if(currentDeciSecond == 0) return
val container = binding.lapContainerLinearLayout
TextView(this).apply {
textSize = 20f // float로 들어가는 점(특이점)
gravity = Gravity.CENTER
setPadding(30)
val minutes = currentDeciSecond.div(10) / 60
val seconds = currentDeciSecond.div(10) % 60
val deciSeconds = currentDeciSecond % 10
text = "${container.childCount.inc()}. " + String.format(
"%02d:%02d %01d",
minutes, seconds, deciSeconds
)
}.let { lapTextView ->
container.addView(lapTextView, 0) // index를 0부터 넣음으로써 맨 위부터 행이 채워지도록 구현
}
}
TextView를 xml에서 생성하지 않고 현재 컨텍스트에서 사용하겠다 식으로 만듭니다.
내부 속성을 정의하고자 apply 확장함수를 이용하고 let 확장함수를 이용해서 현 TextView 객체를
container로 설정한 LinearLayout에 넣을 수 있도록 합니다.
[MainActivity.kt]
package com.example.chapter6
import android.annotation.SuppressLint
import android.media.AudioManager
import android.media.ToneGenerator
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Gravity
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import com.example.chapter6.databinding.ActivityMainBinding
import com.example.chapter6.databinding.DialogCountdownSettingBinding
import java.util.Timer
import kotlin.concurrent.timer
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var countdownSecond = 10
private var currentDeciSecond = 0 // 1 -> 0.1, 10 -> 1초
private var timerThread: Timer? = null
private var currentCountDownDeciSecond = countdownSecond * 10 // 카운트다운 할 때 초
val handler = Handler(Looper.getMainLooper())
private lateinit var timeRunnable: Runnable
var isRunning = false // 타이머 실행 상태
companion object {
const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.countdownGroup.isVisible = true
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
}
// 카운트다운 클릭
binding.countdownTextView.setOnClickListener {
showCountdownSettingDialog()
}
binding.startButton.setOnClickListener {
//start()
if(!isRunning){
isRunning = true
start2()
}
binding.startButton.isVisible = false
binding.stopButton.isVisible = false
binding.pauseButton.isVisible = true
binding.lapButton.isVisible = true
}
binding.stopButton.setOnClickListener {
showAlertDialog()
}
binding.pauseButton.setOnClickListener {
pause()
binding.startButton.isVisible = true
binding.stopButton.isVisible = true
binding.pauseButton.isVisible = false
binding.lapButton.isVisible = false
}
binding.lapButton.setOnClickListener {
lap()
}
initCountDownViews()
}
private fun initCountDownViews() {
binding.countdownTextView.text = String.format("%02d", countdownSecond)
binding.countdownProgressBar.progress = 100
}
// Handler로 만들기
private fun start2(){
// 핸들러는 UI스레드에서 만들어줄 것
// 메인 루퍼에서 핸들러를 계속 확인하는!
// handler를 통해서 보낼 수 있는것 -> 메시지, Runnable 객체
// object 형태의 Runnable로 접근
timeRunnable = object: Runnable {
override fun run() {
if(!isRunning) return // 실행중이 아니라면 종료
if (currentCountDownDeciSecond == 0) {
currentDeciSecond += 1
val minutes = currentDeciSecond.div(10) / 60
val seconds = currentDeciSecond.div(10) % 60
val deciSeconds = currentDeciSecond % 10
/*
1) runOnUiThread
2) View.post
3) View.postDelayed
4) handler -> handle message or handle runnable object
*/
runOnUiThread {
binding.timeTextView.text = String.format("%02d:%02d", minutes, seconds)
binding.tickTextView.text = deciSeconds.toString()
binding.countdownGroup.isVisible = false
}
} else {
currentCountDownDeciSecond -= 1
val seconds = currentCountDownDeciSecond / 10
val progress =
(currentCountDownDeciSecond / (countdownSecond * 10f)) * 100
binding.root.post {
binding.countdownTextView.text = String.format("%02d", seconds)
binding.countdownProgressBar.progress = progress.toInt()
}
}
if (currentDeciSecond == 0 && currentCountDownDeciSecond < 31 && currentCountDownDeciSecond % 10 == 0) {
val toneType =
if (currentCountDownDeciSecond == 0) ToneGenerator.TONE_CDMA_HIGH_L else ToneGenerator.TONE_CDMA_ANSWER
ToneGenerator(AudioManager.STREAM_ALARM, ToneGenerator.MAX_VOLUME)
.startTone(toneType, 100) // duration 0.1초
}
handler.postDelayed(this, 100) // 100 ms
}
}
handler.post(timeRunnable)
}
// 시간 체크 되는거 -> 워커스레드
private fun start() {
// timer 만드는 것 = 스레드 만드는 것
// 100ms = 0.1초
timerThread = timer(initialDelay = 0, period = 100) {
if (currentCountDownDeciSecond == 0) {
currentDeciSecond += 1
val minutes = currentDeciSecond.div(10) / 60
val seconds = currentDeciSecond.div(10) % 60
val deciSeconds = currentDeciSecond % 10
/*
1) runOnUiThread
2) View.post
3) View.postDelayed
4) handler -> handle message or handle runnable object
*/
runOnUiThread {
binding.timeTextView.text = String.format("%02d:%02d", minutes, seconds)
binding.tickTextView.text = deciSeconds.toString()
binding.countdownGroup.isVisible = false
}
} else {
currentCountDownDeciSecond -= 1
val seconds = currentCountDownDeciSecond / 10
val progress =
(currentCountDownDeciSecond / (countdownSecond * 10f)) * 100
binding.root.post {
binding.countdownTextView.text = String.format("%02d", seconds)
binding.countdownProgressBar.progress = progress.toInt()
}
}
if(currentDeciSecond == 0 && currentCountDownDeciSecond < 31 && currentCountDownDeciSecond % 10 == 0){
val toneType = if(currentCountDownDeciSecond == 0) ToneGenerator.TONE_CDMA_HIGH_L else ToneGenerator.TONE_CDMA_ANSWER
ToneGenerator(AudioManager.STREAM_ALARM, ToneGenerator.MAX_VOLUME)
.startTone(toneType, 100) // duration 0.1초
}
}
}
private fun stop() {
binding.startButton.isVisible = true
binding.stopButton.isVisible = true
binding.pauseButton.isVisible = false
binding.lapButton.isVisible = false
currentDeciSecond = 0
binding.timeTextView.text = "00:00"
binding.tickTextView.text = "0"
initCountDownViews()
binding.countdownGroup.isVisible = true
binding.lapContainerLinearLayout.removeAllViews() // 추가한 뷰들 모두 삭제
isRunning = false
handler.removeCallbacks(timeRunnable)
}
private fun pause() {
timerThread?.cancel() // 취소 = 종료 = 일시정지
timerThread = null // 타이머 초기화
isRunning = false
handler.removeCallbacks(timeRunnable)
}
@SuppressLint("SetTextI18n")
private fun lap() {
if(currentDeciSecond == 0){
return // 시작 안한 상태
}
val container = binding.lapContainerLinearLayout
TextView(this).apply {
textSize = 20f // float로 넣어야함
gravity = Gravity.CENTER
setPadding(30) // 모든 곳에 적용
// 1. 01:03 0 2. 02:08 3 이런식으로 쓰기
// 컨테이너 안에 텍스트뷰 넣어야함
val minutes = currentDeciSecond.div(10) / 60
val seconds = currentDeciSecond.div(10) % 60
val deciSeconds = currentDeciSecond % 10
text = "${container.childCount.inc()}. " + String.format(
"%02d:%02d %01d",
minutes,
seconds,
deciSeconds
)
}.let { lapTextView ->
container.addView(lapTextView, 0) // 맨 위부터 계속 채워지게 Index는 0으로 두기
}
}
private fun showCountdownSettingDialog() {
AlertDialog.Builder(this).apply {
val dialogBinding = DialogCountdownSettingBinding.inflate(layoutInflater)
setView(dialogBinding.root) // subView를 넣는거니까
with(dialogBinding.countdownSecondPicker) {
maxValue = 20
minValue = 0
value = countdownSecond
}
setTitle("카운트다운 설정")
setPositiveButton("확인") { _, _ ->
countdownSecond = dialogBinding.countdownSecondPicker.value
currentCountDownDeciSecond = countdownSecond * 10
binding.countdownTextView.text = String.format("%02d", countdownSecond)
}
setNegativeButton("취소", null)
}.show()
}
private fun showAlertDialog() {
// DatePickerDialog
// TimePickerDialog
// AlertDialog
AlertDialog.Builder(this).apply {
setMessage("종료하시겠습니까?")
setPositiveButton("네") { _, _ ->
stop()
}
setNegativeButton("아니오", null)
}.show()
}
}
전체 코드이며, 모든 내용은 github 주소로 참조해두겠습니다.
https://github.com/Haamseongho/Kotlin_Jetpack/tree/dev_haams_v2/Chapter6
이번 포스팅에서 중요한 것은 스레드 활용 방법이었고 관련 개념에 대해서 Handler로 처리되는 것
runOnUiThread나 View.Post로 처리되는 것 이 여러 방법에 대해서 더 많이 시도해보고
내용을 숙지해보는 것이 좋을 것 같습니다. (참고로 timer는 자동으로 워커스레드로 인식됩니다)
(+ Looper에 대한 개념도 공부해보면 좋을 것 같습니다!)
또한 AlertDialog 사용할 때 커스텀 뷰로 바인딩 접근이 가능하며 setView를 통해서 접목가능한
서비스로 구현할 수 있음을 숙지해야 합니다.
나머지 내용에 대해서는 크게 어려움이 없을테니 같은 결과에 대해서 다양한 코드 구현으로
동일하게 만들어 보는 것도 역량 강화에 좋은 기회가 될 것이라 생각합니다.
이상으로 포스팅을 마치며 저도 스레드와 커스텀 뷰에 대해 더 공부해보고 구현해보도록 하겠습니다.
감사합니다.
'Android' 카테고리의 다른 글
RoomDB 활용 방법과 Coroutine 사용 (0) | 2024.07.29 |
---|---|
단어장앱-1 (RecyclerView) (2) | 2024.07.26 |
+/- 만 구현된 간단한 계산기앱(Decimal Format, Flow, StringBuilder) (0) | 2024.07.23 |
Layer / DatePickerDialog / Spinner / SharedPreference (6) | 2024.07.22 |
Xml - style 구성 / Intent 관리 (0) | 2024.07.21 |