코틀린의 철학

|

코틀린의 주요 특성

정적 타입 언어

코틀린은 정적 언어이기 때문에 컴파일 타임에 모든 객체와 메소드의 타입을 알 수 있습니다. 코틀린은 타입 추론(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())
}

코틀린 확장 함수

|

코틀린은 확장 함수를 지원합니다.

Java의 경우 기존에 만들어진 클래스에 새로운 메소드를 추가하려면 해당 클래스를 상속하는 새로운 클래스를 만들어야 하는데, 코틀린에서는 확장 함수(Extension Function)를 이용해서 상속 없이 기존 클래스에 새로운 함수를 추가할 수 있습니다.

이 때 확장 함수를 추가할 대상 클래스는 리시버 타입(Receiver Type)이라고 합니다.

예를 들어 다음과 같은 방법으로 확장 함수를 추가할 수 있습니다.

fun main(args: Array<String>) {
    println("snowdeer".hello())
}

fun String.hello() : String {
    return "$this, hello"
}

확장 함수의 사용 방법은 클래스 내 정의된 메소드와 동일하게 점(.)을 찍고 호출할 수 있지만, 실제로는 클래스 외부에서 추가된 함수이기 때문에 함수 내에서는 privateprotected로 선언된 변수나 메소드에 접근할 수 없습니다.

Scale 및 Gesture Handler

|

화면의 TouchEvent에서 멀티 터치를 이용한 Scale이나 Gesture를 인식하는 Handler 코드 예제입니다.

package com.snowdeer.utils

import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector

interface OnScaleGestureEventListener {
    fun onSingleTab(x: Float, y: Float)
    fun onDoubleTab(x: Float, y: Float)
    fun onLongPress(x: Float, y: Float)
    fun onScaleChanged(scaleFactor: Float)
    fun onOffsetChanged(offsetX: Float, offsetY: Float)
}

class ScaleGestureHandler(ctx: Context) {

    private var scaleFactor = 1.0F
    private var offsetX = 0.0F
    private var offsetY = 0.0F
    private var scaledOffsetX = 0.0F
    private var scaledOffsetY = 0.0F
    private var focusX = 0.0F
    private var focusY = 0.0F

    private val scaleGestureDetector: ScaleGestureDetector
    private val gestureDetector: GestureDetector
    var onScaleGestureEventListener: OnScaleGestureEventListener? = null

    init {
        scaleGestureDetector = ScaleGestureDetector(ctx, ScaleListener())
        gestureDetector = GestureDetector(ctx, GestureListener())
    }

    fun handleTouchEvent(event: MotionEvent): Boolean {
        scaleGestureDetector.onTouchEvent(event)

        if (gestureDetector.onTouchEvent(event)) {
            return true
        }

        return false
    }

    fun reset() {
        scaleFactor = 1.0F
        offsetX = 0F
        offsetY = 0F
        scaledOffsetX = 0F
        scaledOffsetY = 0F
        focusX = 0.0F
        focusY = 0.0F
    }

    private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            scaleFactor = Math.max(0.8f, Math.min(scaleFactor, 3.0f))

            focusX = detector.focusX
            focusY = detector.focusY

            onScaleGestureEventListener?.onScaleChanged(scaleFactor)

            offsetX = (scaledOffsetX - focusX) * scaleFactor + focusX
            offsetY = (scaledOffsetY - focusY) * scaleFactor + focusY

            onScaleGestureEventListener?.onOffsetChanged(offsetX, offsetY)

            return true
        }
    }

    private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
        override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
            onScaleGestureEventListener?.onSingleTab(e.x, e.y)
            return true
        }

        override fun onDoubleTap(e: MotionEvent): Boolean {
            onScaleGestureEventListener?.onDoubleTab(e.x, e.y)
            return true
        }

        override fun onLongPress(e: MotionEvent) {
            onScaleGestureEventListener?.onLongPress(e.x, e.y)
        }

        override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
            scaledOffsetX -= distanceX / scaleFactor
            scaledOffsetY -= distanceY / scaleFactor

            offsetX = (scaledOffsetX - focusX) * scaleFactor + focusX
            offsetY = (scaledOffsetY - focusY) * scaleFactor + focusY

            onScaleGestureEventListener?.onOffsetChanged(offsetX, offsetY)

            return true
        }
    }
}

Runtime Permission (Kotlin 버전)

|

기존에 Java 버전의 Runtime Permission을 포스팅 했었지만 이번에는 Kotlin 버전으로 포스팅합니다.

private const val PERMISSION_REQUEST_CODE = 1231

class MainActivity : AppCompatActivity() {

    private val permissions = arrayOf(
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        if (!checkPermissions(permissions)) {
            requestPermissions(permissions, PERMISSION_REQUEST_CODE)
        }
    }

    private fun checkPermissions(permissions: Array<String>): Boolean {
        for (permission in permissions) {
            if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                return false
            }
        }
        return true
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            PERMISSION_REQUEST_CODE -> {
                if ((grantResults.isNotEmpty()) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {

                } else {
                    Toast.makeText(applicationContext, "Permission is not granted.", Toast.LENGTH_SHORT).show()
                }
            }

        }
    }
}

build.gradle 에서 변수 사용하기

|

gradle에서 변수를 사용하는 방법입니다.

예를 들어 특정 라이브러리의 버전을 변수로 지정할 수 있습니다.

build.gradle (Project)

buildscript {
    ext.kotlin_version = '1.3.41'
    ext.snowlib_version = 'latest.integration'

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}


build.gradle (Module)

그리고 아래 예제와 같은 방법으로 위에서 선언한 snowlib_version이라는 변수를 사용할 수 있습니다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation project(':common_library')
    implementation 'com.google.code.gson:gson:2.8.5'

    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:design:28.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation group: 'snowdeer.message.utils', name: 'snowdeer-message-util', version: "$snowlib_version", changing: true
    implementation group: 'snowdeer.actiontool.library', name: 'snowdeer-actiontool-library', version: "$snowlib_version", changing: true

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}