Android anko with kotlin

안드로이드 Anko, Kotlin으로 살펴보기

Droid Knights 팀에서 준비한 올어바웃 코틀린 (AAK: All About Kotlin) 행사에서 “안드로이드 Anko, Kotlin으로 살펴보기”라는 주제를 다룬 세션입니다.


소개

Kotlin을 사용한 지는 1년 남짓 되었고 Anko도 계속 사용해 왔습니다. Anko라고 하면 안드로이드에서 XML을 대체해서 뷰를 그릴 수 있는 라이브러리라고 생각할 수 있지만, Anko에서는 공식적으로 안드로이드 개발을 쉽고 빠르게 할 수 있는 라이브러리라고 정의하고 있습니다. 레이아웃뿐만 아니라 다른 기능도 있습니다.

Anko

Anko를 사용하고자 하는 분이라면 잘 정리된 공식 GitHub의 Wiki를 참고하면 좋을 것 같습니다. Anko는 다음 크게 네 가지로 나눠집니다.

  • Anko Commons
  • Anko Coroutines
  • Anko SQLite
  • Anko Layouts
dependencies {
  compie "org.jetbrains.anko:anko:$anko_version"
}

이 네 가지 기능을 전부 사용하려면 위와 같이 디펜던시를 추가하면 됩니다. 하지만 이 중 하나만 사용한다면 쓰고 싶은 것만 따로 추가할 수 있습니다. 특히 레이아웃이 가장 기능이 많으므로 주로 레이아웃을 사용하게 될 확률이 높긴 합니다.

Anko Commons

Dialog와 관련된 유틸 클래스를 Kotlin에서 사용할 수 있는 일종의 모음집입니다. toast에 문자열만 넘겨서 사용하거나, alert나 selector 창을 띄우거나 할 수 있습니다. 인텐트도 간단하게 쓸 수 있도록 도와줍니다. 브라우저를 띄우거나 텍스트를 보내거나 메일을 보내면 이 라이브러리를 사용하면 쉽게 쓸 수 있습니다. 안드로이드를 개발할 때 가독성도 높아집니다.

Anko Coroutines

kotlinx,coroutines 라이브러리를 기본으로 한 유틸리티입니다.

Anko SQLite

ORM의 일종인데, 저는 Realm이라는 좋은 라이브러리를 사용하고 있어서 따로 사용해보진 않았습니다.

Anko layouts

Anko layouts는 Kotlin으로 작성된 안드로이드 DSL(Domain-Specific Language)라고 정의하고 있습니다. 기존에 안드로이드에서 XML로 작업하는 뷰는 다음과 같은 단점을 가졌습니다.

  • 코드 재활용 불편
  • 불필요한 XML 파싱으로 인한 CPU 및 배터리 소모

Anko는 이런 단점을 극복하는 라이브러리로, 작성이 쉽고 가독성이 높으며, 런타임에 XML 파싱에 따르는 오버헤드를 없앨 수 있습니다. 이제 Anko로 만드는 코드를 보여드리겠습니다.

가독성이 떨어지던 위 코드를 아래 코드처럼 작성해서 사용할 수 있습니다.

Anko layouts를 사용할 때의 성능 향상을 벤치마킹한 결과입니다. 최대 600%까지 성능이 향상되는 것을 볼 수 있습니다. 하지만 절대적인 수치는 ms 단위이므로 체감상 성능 향상이 크지 않을 수 있습니다. 단, 뷰가 점점 복잡해질수록 성능 향상을 체감할 수 있게 될 겁니다.

이런 개발 뉴스를 더 만나보세요

Anko layouts 사용 방법


override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  
  verticalLayout {
    padding = dip(30)
    editText {
      hint = "Name"
      textSize = 24f
    }
    editText {
      hint = "Password"
      textSize = 24f
    }
    button("Login") {
      textSize = 26f
    }
  }
}

액티비티에서 사용할 때는 원래 onCreate에서 XML ID를 넘겨주던 코드 대신 onCreate에 바로 코드를 써줄 수 있습니다. 이렇게 하면 내부적으로 setContentView를 호출해서 액티비티에 뷰를 적용해줍니다.


class MyActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onCreate(savedInstanceState, persistentState)
    MyActivityUI().setContentView(this)
  }
}

class MyActivityUI : AnkoComponent<MyActivity> {
  override fun createView(ui: AnkoContext<MyActivity>) = with(ui) {
    verticalLayout {
      val name = editText()
      button("Say Hello") {
        onClick { ctx.toast("Hello, $(name.text)!") }
      }
    }
  }
}

혹시 액티비티에 뷰 코드를 쓰지 않고 따로 클래스로 빼고 싶다면, 위 코드처럼 Anko에서 제공하는 Anko Component를 이용할 수 있습니다. AnkoComponent를 상속받아서 createView를 오버라이드하고 레이아웃을 쓰고 onCreate에서 호출합니다.


override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View
        = FragmentUi<Fragment>().createView(AnkoContext.create(ctx, this))

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    return UI {
        verticalLayout {
            linearLayout {
                avatar = imageView().lparams(width = dip(48), height = dip (48))
                name = textView().lparams(width = 0, weight = 1f)
            }

            linearLayout {
                // ...
            }
        }
    }.toView()
}

프래그먼트에서도 비슷하게 사용할 수 있습니다. 아래 코드를 보면 Anko에서 제공하는 UI 래퍼를 사용해서 뷰를 그리고 toView로 반환하면 프래그먼트에서 뷰를 그릴 수 있습니다. 혹은 위 코드와 같이 Anko Component를 사용할 수도 있습니다.

기존에는 Data Binding을 사용해야 했지만 Anko Layouts를 사용해서 런타임에 원하는 그림을 그리거나 동적으로 그릴 수도 있습니다. MVP 패턴에도 도움이 됩니다. 이제 Anko Layout의 구현과 내부 동작을 좀 더 자세히 살펴보도록 하겠습니다.

Anko layouts 동작 방법

Anko가 어떻게 Kotlin을 이용해서 저런 모습의 코드를 만들어내는지 원래 Kotlin 코드에서 차근차근 단계별로 변경해보겠습니다. 공식 홈에서는 Anko가 마법이 아니라 Kotlin의 익스텐션 함수와 속성을 안전한 형 변환 빌더(type-safe builder)로 만든 것이라고 설명하고 있습니다. 빌더라는 컨셉은 Groovy 커뮤니티에서 시작됐으며 대표적으로는 동적인 Gradle 빌드 파일이 있습니다. 한편 Kotlin은 정적이므로 안전한 형 변환 빌더라고 할 수 있습니다.

안전한 형 변환 빌더란 Kotlin의 언어적인 특성을 이용해서 XML이나 UI 컴포넌트를 보기 쉽게 그려주는 개념입니다. Higher-Order function, Extension function, Function Literals With Receiver와 같은 Kotlin 특성을 활용합니다.


fun higherOrderSingle(x: Int, func: (Int) -> Int): Int {
  return func(x)
}

higherOrderSingle(1, { x -> x + 1 })
higherOrderSingle(1, { it + 1 })
higherOrderSingle(1) { it + 1 }
higherOrderSingle(1) {
  it + 1
}

Higher-Order function 코드입니다. 안드로이드의 클릭 이벤트 등에서 람다 표현식을 쓰거나 레트로 람다를 이용해서 많이 쓰므로 익숙한 표현일 겁니다. Kotlin에서는 라이브러리를 쓸 필요 없이 바로 함수를 넘겨 줄 수 있으며 이를 표현하는데 여러 가지 방식을 사용할 수 있습니다. 특히 마지막 줄에서 매개 변수가 람다이고 하나 있을 때 중괄호로 바로 람다 식을 넣어줄 수 있는 컨벤션을 볼 수 있으며, Anko는 이런 형태를 사용합니다.


inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV {
  val constr = TV::class.java.getConstructor(Context::class.java)
  val view = constr.newInstance(context)
  view.init()
  return view
}

val view = v<TextView>(context) {
  layoutParams = layoutParams(WRAP_CONTENT, WRAP_CONTENT)
  text = "Hello"
}

Anko 함수를 만들어 봤습니다. 두 개의 매개 변수를 받고 있고 뒤 변수는 함수 리터럴입니다. Higher-Order function 형태의 제네릭입니다. 제일 먼저 보이는 inline 키워드는 컴파일 시에 코드를 바꿔서 변수가 메모리에 묶여 있을 때 발생할 수 있는 오버헤드를 없애주는 기능을 합니다. 또한, 제네틱 타입 앞에 위치한 reified 키워드는 컴파일 타임에 JVM 클래스 오브젝트에 접근하여 함수의 타입을 특정합니다. 따라서 코드에서 TV 제네릭 타입은 반드시 View, 혹은 View의 하위 클래스로 특정됩니다. Java의 제네릭은 컴파일 때 타입을 체크하지만 실제로 타입 정보를 유지하지는 않으므로 캐스트 경고가 뜨는데, reified를 붙이면 이 경고를 없앨 수 있습니다. 물론 컴파일 때 약간의 오버헤드가 추가되겠죠.

두 번째 변수는 init이라는 이름의 리시버를 갖는 함수 리터럴을 매개 변수로 가집니다. 이렇게 하면 함수 블럭에 리시버의 오브젝트가 this로 레퍼런스돼서 TV의 멤버 변수나 함수를 바로 호출할 수 있습니다. TV의 getConstructor를 이용해 생성자를 가져와서 인스턴스를 만들고 이 인스턴스가 람다 블럭에 invoke 돼서 init 함수가 호출됩니다. 다음으로 생성된 TV 객체를 반환합니다.

아래 코드에서 볼 수 있듯 v 함수 첫 매개 변수에 context가, 두 번째 매개 변수에 람다가 들어가며, 이 코드 블럭 안에서 TextView의 멤버 함수와 변수에 바로 접근할 수 있습니다.

Kotlin을 사용할 때 필수로 쓰는 몇 가지 함수 중 run, let, apply, use, with 등이 리시버를 갖는 함수 리터럴을 이용하여 만들어져 있습니다. 이 함수들의 원형을 보면서 어떻게 동작하는지 살펴보는 것도 좋을 것 같습니다. 다음 코드로 넘어가 볼까요?


inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV {
  val constr = TV::class.java.getConstructor(Context::class.java)
  val view = constr.newInstance(context)
  view.init()
  return view
}

inline fun <reified TV : View> v(parent: ViewGroup, init: TV.() -> Unit) : TV {
  val constr = TV::class.java.getConstructor(Context::class.java)
  val view = constr.newInstance(parent.context)
  parent.addView(view)
  view.init()
  return view
}

안드로이드 뷰는 상위 뷰에 하위 뷰가 추가되는 형태로 만들어지기 때문에 ViewGroup을 받는 함수를 하나 더 만들었습니다. addView를 호출하는 것만 다르고 나머지는 동일합니다. 이 두 함수를 이용하여 뷰를 표현해 보겠습니다.


val view = v<LinearLayout>(context) {
  layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
  orientation = VERTICAL
  
  v<TextView>(this) {
    layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    text = "Hello"
  }
  
  v<TextView>(this) {
    layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    text = "World"
  }
}

위 코드는 뷰를 표현하고 있는데, 앞서 봤던 Anko 코드와 조금 비슷해졌지만, 아직 xml 보다 읽기가 불편합니다. 이때 Kotlin의 확장 함수 기능을 이용하면 더욱 가독성을 높일 수 있습니다. 먼저 첫 번째 매개 변수로 받는 context, viewGroup 등을 줄여보겠습니다.


// inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV {
inline fun <reified TV : View> Context.v(init: TV.() -> Unit) : TV {
  val constr = TV::class.java.getConstructor(Context::class.java)
  
  // val view = constr.newInstance(context)
  val view = constr.newInstance(this)
  view.init()
  return view
}

//inline fun <reified TV : View> v(parent: ViewGroup, init: TV.() -> Unit) : TV {
inline fun <reified TV : View> ViewGroup.v(init: TV.() -> Unit) : TV {
  val constr = TV::class.java.getConstructor(Context::class.java)
  
  // val view = constr.newInstance(context)
  val view = constr.newInstance(context)
  parent.addView(view)
  view.init()
  return view
}

기존에 만든 v 함수들을 각각 Context와 ViewGroup의 확장 함수로 만들면 context와 ViewGroup을 매개 변수로 받을 필요 없이 this를 이용할 수 있습니다. viewGroup은 안에 context를 포함하고 있으므로 context에 접근할 수 있죠.


v<LinearLayout>(context) {
  layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
  orientation = VERTICAL
  
  v<TextView>(this) {
    layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    text = "Hello"
  }
  
  v<TextView>(this) {
    layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    text = "World"
  }
}

좀 더 보기가 좋아지긴 했지만, 아직 완전하지 않습니다. v<LinearLayout>, v<TextView>, v<TextView처럼 제네릭을 쓴 부분을 바꿔 보겠습니다. 확장 함수를 이용하여 함수를 만들어 주면 됩니다.


fun ViewGroup.linearLayout(init: LinearLayout.() -> Unit) = v(init)
fun Context.linearLayout(init: LinearLayout.() -> Unit) = v(init)

fun ViewGroup.textView(init: TextView.() -> Unit) = v(init)
fun Context.textView(init: textView.() -> Unit) = v(init)

Context와 ViewGroup의 확장 함수로 linearLayout, textView라는 이름의 확장 함수를 만들어서 v() 함수를 호출하도록 합니다. 이제 v를 호출하는 것이 아니라 linearLayout 또는 textView를 호출할 수 있습니다.


linearLayout {
  layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
  orientation = VERTICAL
  
  textView {
    layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    text = "Hello"
  }
  textView {
    layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    text = "World"
  }
}

이제 먼저 봤던 Anko 예제 코드와 유사해진 것을 확인할 수 있습니다. Anko는 이런 방식으로 Kotlin의 기능을 이용해서 만들어졌습니다. 마찬가지로 Gradle도 이런 Kotlin의 원리를 이용하여 빌드 스크립트를 짤 수 있으므로 Kotlin을 도입하지 않았나 합니다.

참고 자료


본 영상과 글은 올어바웃 코틀린 (AAK: All About Kotlin)의 비디오 스폰서인 Realm에서 제공합니다. 모바일 개발자가 더 나은 앱을 더 빠르게 만들도록 돕는 Realm 모바일 데이터베이스Realm 모바일 플랫폼을 통해 핵심 로직에 집중하고 개발 효율을 높여 보세요! 공식 문서에서 단 몇 분 만에 시작할 수 있습니다. 또한 Realm 아카데미에서는 모바일 개발자를 위한 다양한 최신 기술 뉴스와 튜토리얼을 제공하고 있으니 즐겨찾기하고 자주 들러 주세요!

다음: Kotlin를 마스터하는 길 #6: Kotlin과 Anko로 Android 개발하기

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

이대명

4 design patterns for a RESTless mobile integration »

close