코루틴(Coroutine) Pause/Resume 사용하기

|

SAM 변환

import android.os.Bundle
import android.os.Handler
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume

class MainActivity : AppCompatActivity() {

    private var continuation: Continuation<String>? = null

    private var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.i("snowdeer", "[snowdeer] onCreate()")

        run_button.setOnClickListener {
            startCoroutine()
        }
        next_button.setOnClickListener {
            continuation?.resume("Resumed")
        }

    }

    private fun startThread() {

    }

    private fun startCoroutine() {
        Log.i("snowdeer", "[snowdeer] startCoroutine()")

        GlobalScope.launch {
            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()
        }

        Log.i("snowdeer", "[snowdeer] end of startCoroutine()")
    }

    private fun refreshCount() {
        count++
        count_textview.text = "$count"
    }

    private suspend fun pause() = suspendCancellableCoroutine<String> {
        continuation = it
    }
}

SAM(Single Abstract Method)

|

SAM 변환

코틀린은 자바에서 작성한 인터페이스에 대해 SAM(Single Abstract Method) 변환을 지원합니다. 그래서 인터페이스를 매개변수로 받는 함수에 대해서 인터페이스 대신 함수를 전달할 수 있고 코드가 간결해집니다.

대표적인 예제로 View.SetOnClickListener()을 들 수 있습니다.


View.SetOnClickListener 예제 (Java)

button.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        // TODO
    }
});


View.SetOnClickListener 예제 (Kotlin)

button.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View) {
        // TODO
    }
})


람다를 이용해 인터페이스 대신 함수를 전달하는 코드 (Kotlin)

button.setOnClickListener({ v: View ->
    Unit
    // TODO
})


SAM 변환 후 간소화된 코드 (Kotlin)

button.setOnClickListener {
    // TODO
}

와 같은 형태가 됩니다. 하나의 함수만을 포함하는 인터페이스는 이와 같이 단순하게 표현할 수 있습니다.

범위 지정 함수(let, apply, with, run, also)

|

범위 지정 함수

범위 지정 함수는 중괄호({ })로 묶여 있는 부분에 전체적으로 적용되는 함수이며, 코틀린에서는 let, apply, with, run, also 등과 같은 함수들을 제공하고 있습니다.

이 함수들은 전부 비슷한 기능을 하며, 사용법 또한 비슷합니다.


let

let은 함수를 호출한 객체를 이어지는 블록의 매개변수로 전달하는 역할을 합니다.

함수의 정의는 다음과 같이 되어 있습니다.

fun <T, R> T.let(block: (T) -> R): R


예를 들면 다음과 같은 코드를 작성할 수 있습니다.

fun sample(message:String) {
    message.let {
        Log.i("message: $it")
    }
}

message라는 매개변수를 let으로 다음 블럭에 넘기고, 그 안에서는 it 키워드를 이용해서 message를 사용할 수 있습니다.

다음과 같이 ? 키워드와 같이 사용해서 Null Check 용도로도 사용할 수 있습니다.

fun sample(message:String?) {
    message?.let {
        Log.i("message: $it")
    }
}


apply

applylet과 사용법은 비슷하지만 블럭으로 넘긴 매개변수가 it이 아니라 this라는 점에서 차이가 있습니다.

함수의 정의는 다음과 같습니다.

fun T.apply(block: T.() -> Unit): T


private val timelineGuideLinePaint = Paint()

timelineGuideLinePaint.apply {
    color = context.getColor(R.color.colorTimelineGuideLine)
    style = Paint.Style.STROKE
    pathEffect = DashPathEffect(floatArrayOf(20F, 10F), 20F)
    strokeWidth = 1f
}


with

withapply와 비슷한 용도로 사용됩니다. 다만, with는 인자를 가지며, 해당 인자를 다음 함수 블럭으로 전달합니다.

함수 정의는 다음과 같습니다.

fun <T, R> with(receiver: T, block: T.() -> R): R


fun sample() {
    with(textView) {
        text = "hello, snowdeer"
    }
}


run

run 함수는 인자없이 사용하는 익명 함수처럼 사용하는 방법과 객체에서 호출하는 형태를 제공하고 있습니다.

함수 정의는 다음과 같습니다.

fun run(block: () -> R): R

fun <T, R> T.run(block: T.() -> R): R


val result = run {
    val a = getResult(100, 50)
    val b = getResult(1000, 2000)
    
    a + b
}

fun printName(person: Person) = person.run {
    print("${person.name}")
}

run 내부에서 선언되는 변수들은 블록 외부에 노출되지 않기 때문에 변수 영역을 명확하게 분리할 수 있습니다

Drag and Drop 시 Custom ShadowBuilder 사용하기

|

안드로이드에서 드래그&드랍(Drag & Drop)시 보여주는 반투명 이미지는 View.ShadowBuilder 클래스를 통해서 만들 수 있습니다. 자동으로 해당 View에서 반투명 이미지를 생성해주는데, 만약 개발자가 원하는 특정 이미지가 있다면 다음 코드를 이용해서 해당 이미지를 드래그시 사용할 수 있습니다.

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.view.View

class ImageShadowBuilder : View.DragShadowBuilder() {

    private var shadow: Drawable? = null

    companion object {

        fun fromResource(ctx: Context, resId: Int): View.DragShadowBuilder {
            val builder = ImageShadowBuilder()
            builder.shadow = ctx.resources.getDrawable(resId)

            builder.shadow?.let {
                it.setBounds(0, 0, it.minimumWidth, it.minimumHeight)
            }

            return builder
        }

        fun fromBitmap(ctx: Context, bm: Bitmap): View.DragShadowBuilder {

            val builder = ImageShadowBuilder()
            builder.shadow = BitmapDrawable(ctx.resources, bm)

            builder.shadow?.let {
                it.setBounds(0, 0, it.minimumWidth, it.minimumHeight)
            }

            return builder
        }

    }

    override fun onDrawShadow(canvas: Canvas?) {
        shadow?.draw(canvas)
    }

    override fun onProvideShadowMetrics(outShadowSize: Point?, outShadowTouchPoint: Point?) {
        outShadowSize?.x = shadow?.minimumWidth
        outShadowSize?.y = shadow?.minimumHeight

        outShadowTouchPoint?.x = (outShadowSize?.x ?: 0 / 2)
        outShadowTouchPoint?.y = (outShadowSize?.y ?: 0 / 2)
    }
}

Drag and Drop 구현

|

안드로이드에서 드래그&드랍(Drag & Drop) 기능은 API로 제공을 해주기 때문에 아래와 같은 요소만 구현해주면 간편하게 구현할 수 있습니다.

  • Drag를 위해 클립보트(Clipboard)에 저장할 내용
  • Drag 동안 화면에 반투명하게 보여줄 이미지
  • Drag 이벤트를 처리하는 부분


Drag 시작

private fun onLongClick(View view) {
        val data = ClipData.newPlainText("message", "hello")
        val builder = View.DragShadowBuilder(view); 

        view.startDragAndDrop(data, builder, view, 0)
}


Drag 이벤트 처리

setOnDragListener 인터페이스를 구현해주면 됩니다. 여기서 주의할 점은 return true 부분입니다. true로 리턴해야만 DragEvent.ACTION_DROP 등의 이벤트를 수신할 수 있습니다. 만약 false를 리턴하게 되면, DragEvent.ACTION_DRAG_STARTED 이벤트만 수신하며 나머지 이벤트는 받지 못합니다.

override fun onDrag(v: View?, event: DragEvent?): Boolean {
        when(event?.action) {
            DragEvent.ACTION_DRAG_STARTED -> {
                Log.i("[snowdeer] ACTION_DRAG_STARTED()")

            }

            DragEvent.ACTION_DROP -> {
                Log.i("[snowdeer] ACTION_DROP(${event.x}, ${event.y})")

                // TODO : Item을 Drop 했을 때 처리
            }
        }

        return true
    }