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
    }

코틀린의 철학

|

코틀린의 주요 특성

정적 타입 언어

코틀린은 정적 언어이기 때문에 컴파일 타임에 모든 객체와 메소드의 타입을 알 수 있습니다. 코틀린은 타입 추론(Type Inference)을 지원하고 있는데, 역시 컴파일 시점에 타입을 확정하고 검증합니다.

함수형 프로그래밍

코틀린은 함수형 프로그래밍을 지원합니다. 함수를 변수에 담을 수 있고, 파라메터 등으로 전달도 가능합니다. Java의 경우도 Java8 부터 어느 정도 지원하긴 합니다.


코틀린의 철학

실용성

코틀린은 기존의 Java의 불편한 점을 개선하고 간소화하는 방향으로 만들어진 실용적인 언어입니다.

  • Null에 대한 고민이 많이 사라졌습니다.
  • 타입 추론으로 코드가 간결해졌습니다.
  • var, val로 개발 실수를 많이 줄여줍니다.

간결성

코틀린은 기존 코드의 복잡하고 긴 코드들을 간소화할 수 있습니다. 예를 들어 gettersetter를 컴파일 시점에 자동으로 생성할 수도 있습니다. Nulltype 체크를 간결하게 할 수 있고, 다양한 람다(Lambda) 함수를 간결한 문법으로 사용할 수 있습니다.

Data Binding의 경우도 기존의 View를 Binding하는 방법보다 간결합니다.

안정성

NullPointerException을 컴파일 시점에 잡아 줄 수 있기 때문에 보다 쉽게 안정적인 코드를 작성할 수 있습니다.

상호 운용성

기존에 Java에서 사용하던 코드나 라이브러리들을 코틀린에서 그대로 사용할 수 있습니다. 또한 혼용해서 사용도 가능하며, 컴파일하고 나면 Java와 같이 *.class 파일이 생성됩니다. 반대로 코틀린에서 작성한 코드를 Java에서 대부분 사용가능합니다. (100%는 아니고 특정 경우엔 약간의 코드 수정이 필요하기도 합니다.)


Java 대비 코틀린의 새로운 부분들

제어구조 식

if, when, try/catch 등의 제어 구조가 식으로 될 수 있습니다.

val a = 1
val b = 2
val c = 3

val ret = a + b + if(c == 3) c else 0

when

when 명령어는 Java의 switch의 확장 개념입니다. 객체끼리 비교도 가능하며 구문이 아니라 식으로 사용도 가능합니다. 다음 예제와 같은 형태로도 사용 가능합니다.

fun cases(obj: Any) = 
    when(obj) {
        1 -> "One"
        "hello" -> "snowdeer"
        is Long -> "obj is Long."
        else -> "Unknown"
    }

Null Safety

?를 이용해서 Null이 될 수 있는 타입과 될 수 없는 타입을 명시적으로 구분합니다.

확장 함수

어떤 클래스를 상속받지 않더라도 특정 클래스에 메소드를 추가할 수 있습니다.

fun String.getCenterChar() : Char = 
    this.get(this.lastIndex/2)

fun test() {
    println("Hello, snowdder".getCenterChar())
}