[Android] 코루틴(Coroutine)을 활용한 카운트다운 타이머 만들기

Android/Android · 2021. 2. 7. 23:24
반응형

 

 코루틴을 활용하여 간단한 카운트 다운 타이머를 만들어 보겠습니다.

 

 1. MVVM

 2. Data Binding, ViewBinding

 3. ViewModel Scope

 

 위 3가지 기술을 사용하였으며, 간단한 예제인 만큼 쉽게 보실 수 있습니다.

 간단하게 정의만 알고 계시면 더욱 이해가 빠르실 겁니다.

 

 먼저 코루틴과 LiveData, DataBinding, ViewBinding, viewModel을 사용하기 위해 다음 코드를 build.gradle추가해 주세요.

 

plugins {
'''
    id 'kotlin-kapt'
}

'''

buildFeatures {
    dataBinding = true
    viewBinding = true
}
    
'''

dependencies {

	''''

    def lifecycle_version = "2.2.0"
    def activity_version = "1.1.0"

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // lifecycle
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    // coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
    // by viewModels()
    implementation "androidx.activity:activity-ktx:$activity_version"
}

 

 implementation "androidx.activity:activity-ktx:$activity_version"

 

 이 항목은 ViewModelProviders.of()deprecated 되어서 viewModel을 참고할 수 있는 새 방법입니다. 

기존 항목보다 사용법이 더욱 간단합니다.

 

 

<?xml version="1.0" encoding="utf-8"?>

<layout 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">

    <data>

        <variable
            name="viewmodel"
            type="hello.world.mvvm_timer.CountViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="32dp"
            android:text="@{viewmodel.countText}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:orientation="horizontal"
            android:padding="12dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_time">

            <Button
                android:id="@+id/start_btn"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="5dp"
                android:layout_weight="1"
                android:onClick="@{() -> viewmodel.decreaseCountText()}"
                android:text="Start"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Button
                android:id="@+id/pause_btn"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="5dp"
                android:layout_weight="1"
                android:onClick="@{() -> viewmodel.pauseCount()}"
                android:text="Pause"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

 activity.xml은 간단하게 설정하겠습니다. DataBinding을 사용하기 위해 layout 태그를 포함시켰습니다.

 

 

package hello.world.mvvm_timer

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.TokenWatcher
import android.widget.Toast
import androidx.activity.viewModels
import hello.world.mvvm_timer.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private val viewModel: CountViewModel by viewModels()
    private var mBinding: ActivityMainBinding? = null
    private val binding get() = mBinding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.apply {
            viewmodel = viewModel
            lifecycleOwner = this@MainActivity
        }
    }

    override fun onDestroy() {
        mBinding = null
        super.onDestroy()
    }
}

 

 

 MainActivity.ktViewBinding을 사용했습니다. ViewBindingDataBinding과는 다르게 LifeCycleOwner를 알지 못하므로 위 코드와 같이 설정을 해줘야 합니다. 또한, viewModel설정시켜 줍니다. viewModel을 설정해야만 DataBinding에서 사용하는 뷰가 보이게 됩니다. (CountViewModel.kt)

 

 

package hello.world.mvvm_timer

import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*

class CountViewModel(application: Application) : AndroidViewModel(application) {
    val countText = MutableLiveData("10")
    private lateinit var job: Job
    private var cnt = 10
    private var clickCount = 0

    companion object {
        private const val TAG = "ViewModel"
    }

    private fun initJob() {
        job = Job()
    }

    fun decreaseCountText() {
        initJob()
        clickCount++
        val coroutineName1 = job.toString().split("{")
        val coroutineName2 = coroutineName1[1].substring(0, 6)
        if (coroutineName2 == "Active" && clickCount > 1) {
            cancelJob()
        } else {
            viewModelScope.launch(job) {
                for (i in cnt downTo 1) {
                    countText.value = cnt.toString()
                    decreaseCount()
                }
            }
        }
    }

    private suspend fun decreaseCount() {
        cnt--
        delay(1000L)
    }

    private fun cancelJob() {
        if (job.isActive || job.isCompleted) job.cancel()
    }

    fun pauseCount() {
        clickCount = 0
        job.cancel()
        initJob()
    }

    override fun onCleared() {
        super.onCleared()
        job.cancel()
        cnt = 10
    }
}

 

 CountViewModel.kt입니다. countText라는 LiveData를 하나 생성했고, 초기값을 문자열 10으로 초기화했습니다. 그다음, Job 객체를 lateinit으로 생성했습니다. Job 객체를 생성한 이유는, 내가 원하는 때에 viewModelScope를 컨트롤할 수 있기 때문입니다.

 

 

 코드를 간단히 쪼개서 보겠습니다

 

// ViewModel: [JobImpl, Active}@c36ca4d]
val coroutineName1 = job.toString().split("{")
// ViewModel: Active
val coroutineName2 = coroutineName1[1].substring(0, 6)

 

 이 부분은 현재 생성된 Job 객체가 Active 상태인지 확인하는 부분입니다. 최종 결과인 "Activie"라는 문자열을 추출하기 위해 코드를 생성했으며, 이는 Start 버튼을 여러 번 클릭했을 때 Coroutine이 여러개 생성되는 것을 막기 위함입니다. 그래서 밑에 코드를 보시면

 

 

if (coroutineName2 == "Active" && clickCount > 1) {
        cancelJob()
}

private fun cancelJob() {
    if (job.isActive || job.isCompleted) job.cancel()
}

 

 현재 job이 Active이고, clickCount가 1 이상, 버튼을 한 번 눌렀을 때 cancelJob() 메서드를 호출합니다. cancelJob() 메서드는 job이 isActive 또는 isCompleted 상태일 때, job.calcel()을 통해 스레드를 중지시킵니다. 이 방법을 통해, Start 버튼을 여러 번 눌러도 스레드가 여러개 생성되는 것을 막을 수 있습니다.

 

 

else {
    viewModelScope.launch(job) {
        for (i in cnt downTo 1) {
            countText.value = cnt.toString()
            decreaseCount()
        }
    }
}

private suspend fun decreaseCount() {
    cnt--
    delay(1000L)
}

 

 이 부분은 viewModelScope.launch(job)을 통해 스레드를 생성하는 부분입니다. viewModelScope는 ViewModel의 생명 주기를 따르며, ViewModel이 onCleared() 되었을 때, viewModelScope도 자동으로 사라지게 됩니다. 따로 처리할 필요가 없어 사용하기가 편리합니다.

 

 

fun pauseCount() {
    clickCount = 0
    job.cancel()
    initJob()
}

 

 Pause 버튼을 눌렀을 때 실행하는 메서드입니다. clickCount를 0으로 초기화해서 스레드가 다시 시작할 수 있도록 설정합니다.

 

 

 코드를 실행하면, 정상적으로 카운트가 내려가는 것을 볼 수 있고, 당연히 화면을 회전해도 카운트 초기화는 발생하지 않습니다. 

 

 사실 코드를 짜면서 그렇게 마음에 들었던 코드는 아니지만, 간단한 예제인 만큼 참고만 하실 정도로 보시면 좋겠습니다.

 

 간단한 예제인 만큼, 설명도 간단하게 했습니다. LiveData나 MVVM 등 더욱 자세히 알고 싶으면 검색하면 좋은 예제들이 많이 나오니 참고하시기 바랍니다. 

 

 

질문, 지적 환영합니다. 

 

 

 감사합니다.

반응형