devlog of ShinJe Kim

[TIL] 2019-12-19 (목)

|

Today I Learned

  • 안드로이드에서 스타일을 만들때에는 마진까지는 적용하지 않는 것이 좋은 것 같다. 동일한 마진을 매번 적용하는 것이 비효율적인 것 같아 타이틀의 스타일에 마진을 넣었었다. 한참 시간이 지난 이후 레이아웃을 수정하려 하는데 마진이 생각대로 적용되지 않아 스타일을 들어가보니 마진이 적용되어 있었다. 레이아웃은 언제든지 수정될 수 있기 때문에 마진값은 항상 변화할 여지가 있고, 스타일을 적용함으로써 얻는 이점보다는 해당 요소에 바로 바로 적용하는 것의 이점이 더 큰 것 같다.

  • 기존의 LinearLayout과 비교하여 RecyclerView의 이점이 무엇인지 찾아보았다. StackOverflow에 잘 설명된 글이 있었다.
  • 그런데 이 글에는 가장 큰 이점이 빠져있는 것 같다. 바로, 리사이클러뷰는 이름 그대로 뷰 객체를 재활용한다는 것이다. 만약 내가 100개의 항목이 있는 리스트를 만든다고 가정하자. 리사이클러뷰가 아닌 기존의 레이아웃들을 활용하면 100개의 뷰(View) 객체가 생성된다. 하지만 리사이클러뷰를 사용하면 한 화면에 보이는 개수만큼의 뷰가 생성되고, 스크롤을 하며 뷰가 화면을 벗어날 때 해당 뷰들을 버리지 않고 재활용한다. 만약 한 화면에 10개의 리스트 항목이 보인다고 가정하면 총 10개의 객체만 생성되어 이 뷰들이 계속 재활용되는 것이다.
  • 아래는 해당 원문을 번역한 것이다.

    장점

    1. 뷰홀더 패턴(ViewHolder Pattern)

      리스트뷰에서는 뷰홀더 패턴이 권고사항일뿐 강제되지는 않았습니다. 반면 리사이클러뷰에서는 RecyclerView.ViewHolder 클래스를 사용하는 것이 의무사항입니다. 이는 리스트뷰와 리사이클러뷰의 가장 큰 차이점 중 하나입니다. 이러한 특징은 리사이클러뷰를 사용하는 것을 조금 더 복잡하게 만들지만, 기존의 리스트뷰에서 겪은 문제점들은 효과적으로 해결해줍니다.

    2. 레이아웃매니저(LayoutManager)

      리사이클러뷰의 큰 이점 중 하나입니다. 기존의 리스트뷰에서는 수직(vertical)의 리스트뷰만 가능했고, 수평(horizontal)의 리스트뷰를 만드는 공식적인 방법이 없었습니다. 하지만 리사이클러뷰를 사용함으로 인해, 아래의 레이아웃을 만들 수 있습니다.

      i) LinearLayoutManager - 수직/수평 리스트를 모두 지원합니다.

      ii) StaggeredLayoutManager - 핀터레스트같이 엇갈린 모양의 레이아웃을 지원합니다.

      iii) GridLayoutManager - 갤러리 앱과 같이 그리드 형태의 레이아웃을 지원합니다.

      가장 좋은 점은 이 모든 레이아웃을 원하는대로 동적으로 만들 수 있다는 것입니다.

    3. 아이템 애니메이터(Item Animator)

      리스트뷰는 좋은 애니메이션에 대한 지원이 부족하지만, 리사이클러뷰는 완전히 새로운 차원을 열어주었습니다. RecyclerView.ItemAnimator 클래스를 사용함으로써, 뷰에 애니메이션을 적용하는 것이 훨씬 쉽고 직관적이게 되었습니다.

    4. 아이템 데코레이션(Item Decoration)

      리스트뷰에서는 테두리(border)나 구분선(divider)과 같이 아이템들을 동적으로 데코레이팅하는 것이 어려웠습니다. 더 나아가 리사이클러뷰의 RecyclerView.ItemDecorator 클래스는 개발자로 하여금 다양한 부분을 제어할 수 있도록 해줍니다. 하지만 시간이 조금 걸리고 복잡하기는 합니다.

    5. 아이템 터치 리스너(OnItemTouchListener)

      리스트뷰의 AdapterView.OnItemClickListener 인터페이스 덕분에 리스트뷰에서 아이템 클릭을 가져오는 것은 간단했습니다. 나아가 리사이클러뷰의 RecyclerView.OnItemTouchListener는 개발자가 더 강력하고 많은 제어를 할 수 있도록 해줍니다. 하지만 이는 조금 복잡한 일이기는 합니다.

    단점

    1. 리스트뷰보다 조금 더 복잡합니다.

    2. 초보자가 리사이클러뷰를 완전히 이해하는 데에는 시간이 많이 걸릴 수도 있습니다.
    3. 코딩이 불필요하게 어려워질 수도 있습니다.
    4. 리스트뷰를 작업할 때보다 더 많은 시간을 들여야 할 것입니다.

[android] room이란? (feat. sharedpreference와 sqlite)

|

[Android] Room이란? (feat. SharedPreference와 SQLite)

안드로이드의 Room을 공부하려 찾아보니 SQLite를 포함하는 개념이라는 것을 알게 되었다. 그래서 내친김에 안드로이드에서 일반적으로 데이터를 저장하는 방식을 모두 정리하고 비교해보려 한다.

1. SharedPreferences

Geeks for Geeks에 친절한 설명이 나와있었다.

SharedPreferences는 적은 양의 원시 데이터(primitive data; ex. String, int, float, Boolean)를 키(key)/값(value)의 쌍(pair)의 형태로 기기의 저장공간에있는 앱 안에 XML 파일로 저장하는 방식을 의미한다. 예를 들면 “username”이라는 키에 username의 값을 저장하는 방식이다. 그리고 “username”이라는 키로 다시 해당 값을 찾을 수 있다. 그리고 preference를 저장하고 필요할 때 언제든지 가져 오는 데 사용할 수있는 간단한 Shared Preferences API를 사용할 수 있습니다. Shared Preferences 클래스는 API가 데이터를 읽고 쓰고 관리할 수 있는 기능을 제공합니다.

Shared Preferences는 각기 다른 상황에 사용될 수 있습니다. 예를 들어, 사용자 설정을 저장하거나 앱 내에서 다양한 활동에 사용할 수있는 데이터를 저장해야 할 때 사용할 수 있습니다. 아시다시피 onPause()는 액티비티가 백그라운드에 놓이기 전이나 destroy되기 전에 항상 호출되기 때문에, 데이터를 지속적으로 저장하려면 onPause()에 저장하는 것이 좋습니다. 그리고 이 데이터는 액티비티의 onCreate()에서 다시 저장될 수 있습니다. SharedPreferences를 사용하여 저장된 데이터는 애플리케이션 내에서 private하게 유지됩니다. 하지만 Shared Preferences는 액티비티의 instance state와는 다릅니다.

그러니까 Shared Preferences는 소량의 데이터 (소량이라는건 어느 정도까지일까..? 찾아보자) 를 키와 값의 쌍으로 기기에 파일로 저장할 수 있게 해주는 것이다. 그렇다면, Shared Preferences와 액티비티에서 자주 사용하던 Saved Instance State는 어떻게 다를까? Geeks for Geeks에 이 두 개념을 비교한 표도 친절하게 나와있다. 이렇게 비교해보니 명확하게 이해가 된다. Shared Preferences는 세션과 관계없이 영속적으로 보관해야하는 데이터가 있을 때 사용하고, Saved Instance State는 유저의 세선 내에서 액티비티의 instance를 저장하는 데에 사용하는 것이다.

Shared Preferences Saved Instance State
유저 세션이 지나도 데이터가 영속적으로 보관됨(앱이 죽거나 다시 시작되는 경우, 혹은 기기가 다시 시작되는 경우에도) 동일한 유저 세선 내에서 액티비티 인스턴스를 영속적으로 저장함
유저의 설정이나 게임 스코어와 같이 세션이 지나도 보존되어있어야 하는 데이터 선택된 탭이나 액티비티의 현재 상태와 같이 세션이 끝났을 때에 보존되면 안되는 데이터
일반적으로 유저의 preference를 저장하는 데에 사용됨 일반적으로 기기가 회전된 후 상태를 다시 되돌리는 데에(recreate) 사용됨

2. SQLite

SQLite 공식 웹페이지에 가보면 “What is SQLite?”라는 설명이 있다. 발췌하여 번역해보았다.

SQLite란 무엇인가?

SQLite는 C언어로 만들어진 라이브러리이며, 작고(small), 빠르고(fast), 자가수용적이고(self-contained), 고의존적이며(high-reliability), 모든 특징을 가지고 있는(full-featured) SQL 데이터베이스 엔진입니다. SQLite는 전 세계에서 가장 많이 쓰이는 데이터베이스 엔진입니다. SQLite는 모든 모바일 폰과 컴퓨터 내부에 심어져있으며, 사람들이 매일 사용하는 수많은 애플리케이션의 내부에 패키지화되어(bundled inside) 있습니다. 더 많은 정보는 이곳을 클릭해주세요.

SQLite 파일 형식은 안정적이고, 크로스 플랫폼을 지원하고, 과거의 기술들과 호환이 가능(backwards compatible)하며 개발자들이 최소한 2050년까지는 유지할 것을 선언했습니다. SQLite 데이터베이스 파일들은 일반적으로 풍부한 컨텐트를 시스템간에( [1] [2] [3] ) 전송하기 위한 컨테이너로 사용되며, 장기간동안 데이터를 저장하기 위한 형식으로 사용됩니다( [4] ). 1조개가 넘는 SQLite 데이터베이스가 현재 사용중입니다( [5] ).

SQLite의 소스코드public-domain이고 모두에게 어떤 목적으로든 열려있습니다.

무언가 굉장한..! 데이터베이스 엔진이라고 하는 것 같다. 그렇다면 MySQL, Oracle 등의 다른 데이터베이스 엔진들과의 차이점은 무엇일까? 하는 궁금증이 생겼다. 공식 웹사이트를 보니 친절하게도 이에 대한 설명이 나와있다.

SQLite는 다른 종류의 문제를 해결하기 위한 데이터베이스 엔진이기때문에, MySQL/Oracle/PostgreSQL/SQL 등과 같은 다른 클라이언트/서버 데이터베이스 엔진과 직접적으로 비교할 수 없습니다.

클라이언트/서버 SQL 데이터베이스 엔진은 공유된 엔터프라이즈 데이터의 저장소를 구현하는 것을 목적으로 합니다. 클라이언트/서버 SQL 데이터베이스 엔진은 확장성(scalability), 동시성(concurrency), 중앙집중화(centralization), 그리고 통제성(control)을 강조합니다. SQLite는 개별 애플리케이션과 개별 기기에 로컬 데이터 스토리지를 제공하기 위해 노력합니다. SQLite는 경제성(economy), 효율성(efficiency), 신뢰성(reliability), 독립성(independence), 그리고 간결함(simplicity)을 강조합니다.

SQLite는 클라이언트/서버 데이터베이스와 경쟁하지 않습니다. SQLite는 fopen()과 경쟁합니다. (찾아보니 fopen()은 C 언어의 파일 입출력 함수라는데 엄청 빠르다는 것을 강조하는 말 같기도 하다.)

즉 요약하자면 SQLite는 파일 형식으로 데이터를 저장하며, 소규모/로컬/빠른/간결한 데이터 저장을 목적으로 하는 관계형 데이터베이스 엔진이다. SQLite의 사용이 권장되는 곳은 임베디드 기기, IoT, 응용 프로그램 파일 형식, 웹사이트(트래픽이 적거나 중간 정도인), 엔터프라이즈 데이터용 캐시 등이다.

이제 SQLite가 모바일 앱에서 사용하기 좋은 관계형 데이터베이스라는 것은 이해했다. 그렇다면 Room은 무엇일까? 그리고 왜 필요할까?

3. Room

안드로이드 공식 문서를 보니 SQLite 대신 Room이 왜 필요한지에 대한 설명이 나와있다.

Room은 SQLite에 대한 추상화 레이어를 제공하여 원활한 데이터베이스 액세스를 지원하는 동시에 SQLite를 완벽히 활용합니다.

상당한 양의 구조화된 데이터를 처리하는 애플리케이션은 데이터를 로컬로 유지하여 큰 이점을 얻을 수 있습니다. 가장 일반적인 사용 사례는 관련 데이터를 캐싱하는 것입니다. 기기가 네트워크에 액세스할 수 없을 때, 이러한 방식으로 오프라인 상태인 동안에도 사용자가 여전히 콘텐츠를 탐색할 수 있습니다. 나중에 기기가 다시 온라인 상태가 되면 사용자가 시작한 콘텐츠 변경사항이 서버에 동기화됩니다.

Room이 사용자를 대신하여 이러한 문제를 처리하므로 SQLite 대신 Room을 사용하는 것이 좋습니다.

(+ Room 설명 추가 예정)

참고 자료

[TIL] 2019-12-16 (월)

|

Today I Learned

  • onCreateOptionsMenu()onPrepareOptionsMenu()의 차이점은?
    • onCreateOptionsMenu(): 한 번만 호출된다.

      안드로이드 공식 문서 - options menu 중

      시스템이 onCreateOptionsMenu()를 호출하고 나면, 개발자가 입력하는 Menu의 인스턴스를 유지하고 무슨 이유로든 메뉴가 무효화되지 않으면 onCreateOptionsMenu()를 호출하지 않습니다. 그러나 onCreateOptionsMenu()는 초기 메뉴 상태를 생성할 때만 사용하고, 액티비티 수명 주기에서 변경할 때는 사용하지 않습니다.

    • onPrepareOptionsMenu(): 메뉴가 열릴 때마다 호출된다.
    • invalidateOptionsMenu(): onPrepareOptionsMenu()를 다시 로드하도록 강제한다.
  • 화면의 상단에 있는 버튼은 업버튼, 안드로이드 기기 자체에(아래에) 있는 버튼은 백버튼이라고 한다.
    • 이 두 버튼은 이전 액티비티 혹은 프래그먼트로 돌아가도록 동작한다.
    • 결과는 같아보이지만 내부적으로 처리되는 것은 다르다.
    • 백버튼을 사용하는 것은 일시적 내비게이션(temporal navigation)이라고 한다. 백버튼을 누르면 직전의 화면으로 이동한다.
    • 업버튼을 사용하는 것은 계층적 내비게이션(hierarchical navigation)이라고 한다. 업버튼을 누르면 앱 계층의 위로 이동한다.

읽을거리

[Kotlin] 코틀린의 Scope 함수

|

코틀린의 Scope 함수

코틀린 표준 라이브러리는 객체의 컨텍스트 내에서 코드 블럭을 실행하기 위한 목적만을 가진 여러가지 함수를 제공합니다. 이런 함수들을 람다식으로 호출할 때, 이는 임시로 범위(scope)를 형성합니다. 이 범위 내에서는 객체의 이름이 없어도 객체에 접근할 수 있습니다. 이러한 함수를 scope functions라고 합니다. 이러한 scope 함수에는 let, run, with, apply, also의 5가지가 있습니다.

기본적으로, 이 함수들은 동일한 역할을 합니다: 객체에 코드 블럭을 실행하는 것입니다. 다른점은, 이 객체를 어떤 방식으로 블럭 안에서 사용할 수 있는지, 그리고 전체 표현식(expression)의 결과가 어떻게 되는지입니다.

아래는 전형적인 scope function의 사용법입니다:

Person("Alice", 20, "Amsterdam").let {f
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

만약 let 없이 동일한 내용의 코드를 작성한다면, 아래의 코드처럼 새로운 변수를 만들고 그 변수를 사용할때마다 변수의 이름을 반복해서 작성해야할 것입니다.

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

scope 함수는 어떠한 새로운 기술적인 능력을 가져다주는 것이 아닌, 코드를 좀 더 간결하고 읽기 쉽게 만들어주는 것입니다.

scope 함수의 비슷한 점들로 인해, 특정 상황에 딱 맞는 함수를 사용하는 것이 모호할 수 있습니다. 이 때 결정하는 기준은, 작성하고자 하는 코드의 의도와 프로젝트 내에서의 일관성입니다. 아래에서 scope 함수와 각각의 사용 관례의 차이점에 대해 상세히 살펴보겠습니다.

차이점

scope 함수는 본질이 비슷하기때문에, 각각의 차이점을 잘 아는 것이 중요합니다. 각 scope 함수에는 두 가지 큰 차이점이 있습니다.

  • 객체의 컨텍스트를 참조하는 방식
  • 리턴값

컨텍스트 객체(Context object): this 혹은 it

scope 함수의 람다식 내에서 컨텍스트 객체는 실제 이름 대신 짧은 참조 이름으로도 사용할 수 있습니다. 각각의 scope 함수는 컨텍스트 객체에 접근하기 위해 다음 두 가지 방법 중 하나를 사용합니다: 람다 수신자(this)를 사용하거나 람다 인자(it)를 사용합니다. 둘 다 동일한 역할을 하는데, 다른 상황 별로 각각의 장점과 단점에 대해 알아보고 권장하는 방식을 알아보겠습니다.

fun main() {
    val str = "Hello"
    // this
    str.run {
        println("The receiver string length: $length")
        // 아래 코드도 동일한 역할을 합니다
        // println("The receiver string length: ${this.length}")
    }

    // it
    str.let {
        println("The receiver string's length is ${it.length}")
    }
}

this

run, with, applythis 키워드로 람다 수신자로서의 컨텍스트 객체를 참조합니다. 그러므로, 이 람다에서는 객체는 일반 객체 함수로도 사용이 가능합니다. 대부분의 경우, 수신 객체의 멤버에 접근할 때 this를 생략함으로써 코드를 간략하게 만들 수 있습니다. 반면, 만약 this가 생략되면, 수신 객체 멤버와 외부의 객체 혹은 함수를 구분하기가 어려울 수도 있습니다. 따라서 수신자(this)로 컨텍스트 객체를 받는 것은 주로 객체의 함수를 호출하거나 프로퍼티를 할당하는 것과 같이 객체 멤버를 처리하는 람다에 사용하는 것을 권장합니다.

val adam = Person("Adam").apply {
    age = 20        // 이는 this.age = 20 이나 adam.age = 20 과 동일합니다.
    city = "London"
}

it

반면, letalso는 컨텍스트 객체를 람다 인자(argument)로 가집니다. 만약 인자의 이름이 정해지지 않았다면, 해당 객체는 암시적인 기본 이름인 it으로 접근할 수 있습니다. itthis보다 짧으며, 주로 it을 사용한 표현식은 읽기 쉽습니다. 하지만, 객체 함수나 프로퍼티를 호출할 때 this와 같이 암시적인 객체가 없습니다. 그러므로, 객체가 함수 호출에서 주로 인자로 사용될 때에는 컨텍스트 객체를 it으로 가지는 것이 더 낫습니다. 또한 코드 블럭 내에서 여러개의 변수를 사용한다면 it을 사용하는 것이 더 좋습니다.

fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
}

val i = getRandomInt()

또한, 컨텍스트 객체를 인자로 전달할때에는, scope 내에서 컨텍스트 객체의 커스텀 이름을 설정할 수 있습니다.

fun getRandomInt(): Int {
    return Random.nextInt(100).also { value ->
        writeToLog("getRandomInt() generated value $value")
    }
}

val i = getRandomInt()

리턴 값(return value)

이 scope 함수들은 리턴값이 다릅니다:

  • applyalso는 컨텍스트 객체(context object)를 리턴합니다.
  • let, run, with는 람다식의 결과를 리턴합니다.

이 두 옵션은 다음에 어떤 코드를 작성할 것인지에 따라 적절한 함수를 선택할 수 있도록 합니다.

컨텍스트 객체(Context object)

applyalso의 리턴값은 컨텍스트 객체 그 자신입니다. 그러므로, applyalsoside steps으로 호출 체인에 포함될 수 있습니다. 즉, 동일한 객체에 대한 함수 호출을 계속 연결하여 작성할 수 있습니다.

val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
    .apply{
        add(2.71)
        add(3.14)
        add(1.0)
    }
    .also { println("Sorting the list") }
    .sort()

또한, applyalso는 컨텍스트 객체를 반환하는 함수의 반환식에서도 사용될 수 있습니다.

fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
}

val i = getRandomInt()

람다 결과(Lambda result)

let, run, with는 람다 결과를 반환합니다. 따라서 이 scope 함수들은 변수에 연산 결과를 할당할 때나, 연산 결과에 연산을 연결할 때 등의 상황에서 사용할 수 있습니다.

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")

또한, 변수를 위한 임시 scope을 만들기 위해 리턴값을 무시하고 scope 함수를 사용할 수도 있습니다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    val firstItem = first()
    val lastItem = last()
    println("First item: $firstItem, last item: $lastItem")
}

함수(Functions)

상황에 맞는 scope 함수를 선택할 수 있도록 권장되는 방식의 사용 예시와 함께 조금 더 자세히 설명해드리겠습니다. 엄밀히 말하면 많은 상황에서 함수는 교체될 수 있습니다. 따라서 아래의 예시에서는 일반적인 사용 방식을 정의하였습니다.

let

컨텍스트 객체(context object)는 인자(it)로서 사용 가능합니다. 이 컨텍스트 객체의 리턴값(return value)은 람다 결과입니다.

let은 호출 체인의 결과로 하나 혹은 그 이상의 함수를 invoke할 때 사용됩니다. 예를 들어, 다음의 코드는 컬렉션의 두 연산의 결과를 출력합니다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

let으로 아래와 같이 다시 작성해볼 수 있습니다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
    println(it)
    // 필요하다면 더 많은 함수를 호출할 수도 있습니다.
}

만약 코드 블럭에 it을 인자로 가지는 단일 함수가 있다면 람다 대신 메서드 참조(::)를 사용할 수도 있습니다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

let은 종종 널이 아닌 값만을 가지는 코드 블럭을 실행시키는 데에 사용됩니다. 널이 아닌 객체에 동작을 수행하기 위해서는, 해당 객체에 안전한 호출 연산자(safe call operator)인 ?.을 사용하고 람다의 action으로 let을 호출하면 됩니다.

val str: String? = "Hello"
// processNonNullString(str)        // 컴파일 에러: star can be null
val length = str?.let {
    println("let() called on $it")
    processNonNullString(it)        // OK: '?.let { }' 안에서는 'it'이 null이 아님
    it.length
}

let을 사용하는 또 다른 방법은 코드 가독성을 높이기 위해 제한된 scope 내에서의 지역 변수를 사용하는 것입니다. 컨텍스트 객체를 위해 새로운 변수를 정의하기 위해서는, 기본적으로 사용되는 it 대신에 람다의 인자로 변수 이름을 넣어 사용하면 됩니다.

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
    println("The first item of the list is '$firstItem'")
    if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")

with

비 확장함수(non-extension function): 컨텍스트 객체는 인자로 전달되지만, 람다 안에서는 수신자(this)로 사용 가능합니다. 리턴값은 람다 결과입니다.

with는 람다 결과 없이 컨텍스트 객체의 호출 함수에서 사용하는 것을 권장합니다. 아래 코드에서 with“이 객체로, 다음을 수행하라(with this object, do the following.).”로 읽힐 수 있습니다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

with의 또 다른 사용법은 값을 계산하는 데에 사용되는 헬퍼 객체(helper object)의 프로퍼티나 함수를 선언하는 데에 사용하는 것입니다.

val numbers = mutableListOf("oen", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

run

컨텍스트 객체는 수신자(this)로 사용가능합니다. 리턴값은 람다 결과입니다.

runwith와 같은 역할을 하지만, let처럼-컨텍스트 객체의 확장 함수처럼- 호출(invoke)됩니다.

run은 람다가 객체 초기화와 리턴 값 연산을 모두 포함하고 있을 때 유용합니다.

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// let() 함수로 쓰인 위와 동일한 코드:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

run은 수신 객체에서 호출하는 것 이외에, 비확장함수(non-extension function)로도 사용할 수 있습니다. 비확장(non-extension) run은 표현식이 필요한 곳에서 다수의 구문 블럭을 실행할 수 있도록 합니다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findALl("+1234 -FFFF not-a-number)) {
    println(match.value)
}

apply

컨텍스트 객체는 수신자(this)로 사용가능합니다. 리턴값은 객체 자기 자신입니다.

apply는 값을 반환하지 않고, 주로 수신 객체의 멤버를 연산하는 곳에 사용하면 됩니다. apply를 사용하는 가장 일반적인 경우는 객체 생성?(configuration)입니다. 이러한 호출은 “다음의 지시를 객체에 적용하라(apply the following assignments to the object).” 로 읽힐 수 있습니다.

val adam = Person("Adam").apply {
    age = 32
    city = "London"
}    

수신자를 리턴값으로 가짐으로써, apply를 더 복잡한 프로세스를 위한 호출 체인에 쉽게 포함시킬 수 있습니다.

also

컨텍스트 객체는 인자(it)로 사용가능합니다. 리턴값은 객체 자기 자신입니다.

also는 컨텍스트 객체를 인자로 가지는 것과 같은 작업을 수행하는 데에 좋습니다. also는 로깅이나 디버그 정보를 출력하는 것과 같이 객체를 변화시키지 않는 부가적인 작업에 적합합니다. 대부분 프로그램의 로직을 파괴하지 않고도 호출 체인에서 also의 호출을 제거하는 것이 가능합니다.

also를 코드에서 보면, “그리고 또한 다음을 수행하라(and also do the following)”의 의미를 읽을 수 있습니다.

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

함수 선택(Function selection)

아래의 표는 목적에 맞게 scope 함수를 선택하는 데에 도움이 될 수 있게 함수별로 주요 차이점을 정리한 표입니다.

Function Object reference Return value Is extension function
let it Lambda result Yes
run this Lambda result Yes
run - Lambda result No: called without the context object
with this Lambda result No: takes the context object as an argument
apply this Context object Yes
also it Context object Yes

아래는 의도한 목적에 따라 scope 함수를 선택할 수 있는 간략한 가이드입니다.

  • 널이 아닌 객체에 람다를 실행할 때: let
  • 지역(local) scope에서 표현식(expression)을 변수로 선언할 때: let
  • 객체 선언?(configuration): apply
  • 객체 선언?(configuration)과 결과를 계산할 때: run
  • 표현식(expression)이 필요한 곳에 문(statements)을 실행할 때: 비 확장(non-extension) run
  • 부가적인 실행: also
  • 객체에 대한 그룹 함수 호출: with

각기 다른 함수의 사용 용도가 중복되기때문에, 프로젝트나 팀에서의 구체적인 컨벤션에 따라 어떤 함수를 사용할지 정하시면 됩니다.

scope 함수는 코드를 더 간결하게 만드는 방법이지만, 남용하는 것은 피해야합니다: 코드 가독성을 떨어트리고 에러를 발생시킬 수 있기때문입니다. scope 함수를 중첩하여 사용하는 것은 피해야하며, 연속(chaining)하여 사용할 때에는 주의하세요: this 혹은 it의 현재의 컨텍스트 객체와 값을 혼동하기 쉽습니다.

takeIf와 takeUnless

scope 함수와 더불어, 표준 라이브러리에는 takeIftakeUnless가 있습니다. 이 함수들은 호출 체인(call chains)의 객체 상태 확인을 포함시킬 수 있습니다.

predicate가 제공된 객체에서 takeIf가 호출될 때, predicate와 객체가 일치하면 takeIf는 predicate를 리턴하고, 그렇지 않으면 null을 리턴합니다. 따라서, takeIf는 단일 객체를 위한 필터링 함수입니다. 결과적으로, takeUnless는 객체가 predicate와 일치하면 객체를 리턴하고, 일치하지않으면 null을 리턴합니다. 이 객체는 람다 인자(it)로 사용가능합니다.

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

takeIftakeUnless 다음에 다른 함수를 연결하는 경우에는 널체크 혹은 세이프 호출(?.)을 하는 것을 잊지 마세요. 이 함수들의 리턴 값은 nullable이기 때문입니다.

val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
// val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 컴파일 에러
println(caps)

takeIftakeUnless는 scope 함수와 같이 쓸 때 특히 유용합니다. 좋은 사용 방식은, 주어진 predicate와 일치하는 객체에서 코드 블럭을 실행할 수 있도록 takeIftakeUnlesslet과 연결하는 것입니다. 이렇게 하기 위해서는, takeIf를 객체에 호출한 뒤, let을 세이프 호출(?)로 호출하는 것입니다. takeIf는 predicate와 매치되지 않는 객체에 대해서는 null을 리턴하며 let은 호출되지 않습니다.

fun displaySubstringPosition(input: String, sub: String) {
    input.indexOf(sub).takeIf { it >= 0 }?.let {
        println("The substring $sub is found in $input.")
        println("Its start position is $it.")
    }
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

아래는 표준 라이브러리 함수 없이 동일한 함수가 어떻게 작성되는지 보여줍니다.

fun displaySubstringPosition(input: String, sub: String) {
    val index = input.indexOf(sub)
    if (index >= 0) {
        println("The substring $sub is found in $input.")
        println("Its start position is $index.")
    }
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

출처

코틀린 공식문서 - Scope Functions

[TIL] 2019-12-04

|

Today I Learned

코틀린에서 lateinit의 특징

  • var로 선언한 프로퍼티에만 사용할 수 있음
  • 클래스 몸체, Top-Level, 함수 내부에 선언한 프로퍼티에 사용할 수 있음. 주생성자에서는 사용할 수 없음.
  • 사용자 정의 getter/setter를 사용하지 않은 프로퍼티에만 사용할 수 있음.
  • null 허용 프로퍼티에는 사용할 수 없음
  • 기초 타입 프로퍼티에는 사용할 수 없음

fragment를 add하는 것과 replace하는 것의 차이

  • 현재의 fragment를 replace하면 onPause() -> onStop() -> onDestroyView() -> onDestroy() -> onDetach() 순서로 호출됨
  • add하면 현재의 fragment는 아무런 호출도 하지 않음.