이 글은 이것이 안드로이드다 with 코틀린(개정판)를 참고하여 작성하였습니다.
작성자 : 이재성
개발환경은 Windows, Android Studio입니다.
1. Scope
코루틴은 정해진 Scope안에서 실행된다. 아래와 같이 두 가지 Scope가 있다.
Global Scope
앱의 생명 주기와 함께 동작하기 때문에 앱이 실행되는 동안은 별도의 생명주기 관리가 필요하지 않다. 따라서, 앱의 시작부터 종료까지 또는 장시간 실행되어야 하는 코루틴이 있다면 Global Scope를 사용한다.
GlobalScope.launch {
// do something
}
Coroutine Scope
특정 이벤트에 따라 서버로 부터 정보를 가져오거나 File을 여는 용도와 같이 필요할 때만 사용하고 완료되면 닫는 용도로 사용하는 Scope이다.
binding.downloadButton.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
// do something
}
}
2. Dispatchers
코루틴이 실행될 Thread를 정하는 Distpatcher에는 IO, Main, Default, Unconfined가 있다.
Dispatchers.Default
CPU를 많이 사용하는 작업을 Background Thread에서 실행하도록 최적화되어 있는 Dispatcher이다. Android System의 Thread Pool을 사용한다.
Dispatchers.IO
이미지 다운로드, 파일 입출력과 같은 IO를 사용할 때 사용하는 Dispatcher이다.
Dispatchers.Main
Android의 기본 Thread에서 Coroutine을 실행하고 UI와 상호작용에 최적화되어있는 Dispatcher이다.
Dispatchers.Unconfined
다른 Dispatcher와는 달리 특정 Thread 또는 특정 Thread Pool을 지정하지 않는다. 일반적으로는 잘 사용하지 않고 특정 목적을 위해 사용하는 추세이다.
3. Coroutine 실행 : launch, async
코루틴을 실행하기 위해 launch 또는 async를 사용할 수 있다. launch는 상태를 관리할 수 있고 async는 상태를 관리하고 연산 결과까지 반환할 수 있다. launch는 상태를 관리할 수 있고 async는 상태를 관리하고 연산 결과까지 반환할 수 있다.
cancel
코루틴의 동작을 멈추는 상태 관리 method이다. 하나의 Scope 안에 여러 개의 코루틴이 있다면 하위 코루틴도 모두 동작을 멈춘다.
아래 예시는 상위 코루틴에서 cancel을 호출해 하위 코루틴도 중단되는 코드이다.
val job = CoroutineScope(Dispatchers.Default).launch {
val job1 = launch {
for (i in 0..10) {
delay(500)
Log.e("Coroutine", "Result = $i")
}
}
}
binding.stopButton.setOnClickListener {
job.cancel()
}
join
코루틴 Scope 안에 선언된 여러 개의 launch 블록은 모두 새로운 코루틴으로 분기되어 동시에 처리되기 때문에 순서를 정할 수 없다. launch 블록 끝에 join을 사용하면 각각의 코루틴이 순차적으로 실행된다. suspend fun과 유사하게 동작한다.
아래 예시는 두 개의 코루틴 중 join으로 인해 앞의 코루틴의 실행이 완료되어야 뒤의 코루틴이 실행되는 코드이다.
CoroutineScope(Dispatchers.Default).launch() {
launch {
for (i in 0..5) {
delay(500)
Log.e("Coroutine1", "Result = $i")
}
}.join()
launch {
for (i in 0..5) {
delay(500)
Log.e("Coroutine2", "Result = $i")
}
}
}
4. async 반환 값 처리
async는 코루틴 Scope의 연산 결과를 받아 사용할 수 있다. 아래처럼 시간이 오래걸리는 2개의 코루틴을 async로 선언하고 결과값을 처리할 때 await을 사용하면 결과 처리가 완료된 후에 await을 호출한 코드가 실행된다.
CoroutineScope(Dispatchers.Default).async {
val deferred1 = async {
delay(500)
350
}
val deferred2 = async {
delay(500)
200
}
Log.e("Coroutine", "Result = ${deferred1.await() + deferred2.await()}") // 550
}
5. suspend function
코루틴에서 suspend fun이 호출되면 이전까지의 코드 실행이 멈추고 suspend fun의 처리가 완료된 후에 멈춰있던 원래 scope가 다음 코드를 실행한다.
suspend fun subRoutine() {
for (i in 0..10) {
Log.e("SUBROUTINE", "$i")
}
}
CoroutineScope(Dispatchers.Main).launch {
// do something 1
subRoutine()
// do something 2
}
CoroutineScope가 실행되면 do something1이라고 작성된 부분이 실행된 후 suspend fun을 호출한다. suspend fun이 모두 실행된 후에 do something 2 가 실행된다.
여기서 suspend fun은 CoroutineScope 안에서 자동으로 Background Thread처럼 동작한다. 호출한 쪽에서의 코드 실행이 잠시 멈췄지만 Thread의 중단은 없다. 코루틴에서는 부모 루틴의 상태 값을 저장한 후 suspend fun을 실행하고, 종료된 후 부모 루틴의 상태 값을 복원하는 형태로 동작한다. 이는 Thread에는 영향을 주지 않는다.
6. withContext로 Dispatcher 분리
suspend fun을 코루틴 Scope에서 호출할 때 호출한 Scope와 다른 Dispatcher를 사용할 때가 있다. 호출한 코루틴은 Main Dispatcher를 사용하여 UI를 제어하는데, suspend fun은 File I/O에 대한 처리를 하는 경우에 해당한다. 이 경우 withContext를 사용하여 호출되는 suspend fun의 Dispatcher를 IO로 변경할 수 있다.
suspend fun fetchFile(): String {
return "aaaaaa"
}
CoroutineScope(Dispatchers.Main).launch {
// UI 제어 관련
val result = withContext(Dispatchers.IO) {
fetchFile()
}
Log.e("Coroutine", "Result = $result")
}
7. 이미지 다운로드 코드 전문
권한 추가 및 의존성 추가
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
<uses-permission android:name="android.permission.INTERNET" />
ImageUtils 클래스로 코드 분리
class ImageUtils {
suspend fun loadImage(imageUrl: String) : Bitmap {
val url = URL(imageUrl)
val stream = url.openStream() // Url을 stream에 담음
return BitmapFactory.decodeStream(stream) // Bitmap 형태로 변환
}
}
MainActivity
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
bindViews()
}
private fun bindViews() {
binding.downloadImageButton.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
binding.downloadProgressBar.visibility = View.VISIBLE
val url = binding.urlEditText.text.toString()
val bitmap = withContext(Dispatchers.IO) { // background에서 실행
ImageUtils().loadImage(url)
}
binding.downloadImageImageView.setImageBitmap(bitmap)
binding.downloadProgressBar.visibility = View.GONE
}
}
}
}
'GDSC HUFS 3기 > Android with Kotlin Team 1' 카테고리의 다른 글
[1팀] 컨텐트 제공자 (1) | 2021.11.30 |
---|---|
[1팀]카메라 원본 이미지 가져오기 및 갤러리에서 이미지 가져오기 (0) | 2021.11.30 |
[1팀] Thread, Handler, Looper (0) | 2021.11.29 |
[1팀] 15. 권한 허가와 베이스 액티비티 (0) | 2021.11.16 |
[1팀] 14-10. 화면 구성하기 : 탭메뉴 뷰페이저와 리사이클러 뷰 (0) | 2021.11.16 |