Kotlin 으로 안드로이드 개발 시작하기

원티드에 합류하기 전에 이미 Kotlin 이 안드로이드 개발의 공식 언어 채택 내용은 알고 있었고, Java로 개발하는데 있어 8부터 지원하는 람다식Java Functional interface API 등을 사용하는것은 조금 어려운 상태였습니다. 실제 Kotlin으로 개발해봤더니 좋다는 많은 소문(?)을 듣고있던 터라 합류 하게되면 개발을 하는 주 언어로 Java 에서 Kotlin 으로 넘어가 개발 환경을 바꿔야 한다는 생각이 있었습니다.

Kotlin 을 시작한지 얼마 안되었지만, 개인적으로 Kotlin 의 장점은 코드량이 상당히 많이 줄고, 가독성은 떨어지지 않으며, 재미가 있다(=특히 코드를 줄여나가는데 있어) 는 것이라 생각합니다. 아직 Kotlin 사용이 익숙치 않지만, 안드로이드를 Kotlin 으로 개발을 시작하는데 있어 몇 가지 포인트가 되었던 부분들을 간략하게 정리해보려고 합니다.

문법 숙지

코틀린 개발을 시작하기에 앞서 가장 먼저 한 일은 기본 문법부터 숙지하는 것이었습니다. 몇 가지 서적을 참조하여 기본 문법을 몇 차례 훑고난 후 Kotlin 으로 안드로이드 스튜디오를 이용하여 새 프로젝트 생성 - 액티비티 1~2개 정도 만들고, RecyclerView 를 활용하여 ViewType 이 여러개 있다는 가정을 하고 Kotlin 으로 코드를 작성해 보면서 빠르게 적응해 가기 시작했습니다. 처음에는 문법이 익숙하지 않아서 기본 클래스 생성자라던가 조건문, 반복문 등 기본 문법은 생각나지 않으면 바로바로 서적에서 해당 내용을 찾아서 확인하고 코드를 작성하려 노력했습니다. 이 후 RecyclerView.ItemHolder 에 대응될 데이터 클래스도 만들고 Listener 설정하고 등등 … 기본적으로 해야할 것들 간단하게 샘플로 작성하였습니다.

Kotlin 으로 기본 문법 숙지하는 동안, 작업 중간중간 kotlin 이 실제 어떻게Java 로 변환되는지 확인이 필요하다면 Android Studio - Tools - Kotlin - Show Bytecode - Decompile 를 선택하면 확인 가능합니다. 책에 있는 내용을 한번 읽어보고 난 뒤, 샘플 코드를 작성하고 코드를 변환시켜서 확인하면 좀 더 자기 것으로 만드는데 도움이 될 것으로 생각합니다.

이와 별도로 개인적으로 Kotlin으로 개발할 때 유용했던 몇 가지는 별도로 목록을 나열해 보았습니다. 각 내용은 링크를 참조하시면 될 것 같습니다.

Kotlin Android Extensions

하지만 아직까지 코틀린을 써야할 이유를 모르겠던 상태에서 Kotlin Android Extension 을 접하게 되었습니다. 가장 큰 장점은 역시 findViewById 를 안해도 된다는 것이었습니다. 레이아웃 파일에 선언된 id 값을 변수 그대로 사용이 가능해 집니다. (Goodbye findViewById, Goodbye ButterKnife…) 이것은 팀의 코드컨벤션 정리에 많은 도움이 되었습니다. 기존에는 xml 의 id 정의 할 때와 변수 정의할 때를 다르게 한다거나 혹은 xml 에서 id를 부여할 때는 _ 를 이용하는 등 작업자마다 차이가 약간씩 있었으나 Kotlin Android Extensions 추가 이후 xml 에 추가되는 뷰에 ID 할당은 camel 표기법으로 자연스럽게 통일 되었습니다. 적용 방법은 어렵지 않습니다. 먼저 build.gradle 에 다음과 같이 추가하면 됩니다.

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

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

만약 Activity 에서 setContentView 호출 시 전달하는 레이아웃 이름이 activity_main 일 경우에 해당 Activity Kotlin 파일 상단에 아래와 같은 import 를 추가해주면 됩니다.

import kotlinx.android.synthetic.activity_main.*

Kotlin Android Extensions 가 가장 매력적으로 사용될 수 있는 부분은 바로 RecyclerView Adapter 를 구현할 때라고 생각합니다. 예를들어 뷰 타입이 2가지이며, 각 뷰는 텍스트뷰 하나만 가지고 있는 RecyclerView 의 RecyclerView.Adapter 를 자바로 구현하려면 일반적으로 아래와 같습니다. (UIItem 은 단순히 viewType 값과 실제 데이터 모델을 위한 Object 타입 변수만 가지고 있습니다.)

public class SimpleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public static final int TYPE_1 = 1;
    public static final int TYPE_2 = 2;

    private final Context context;
    private final List<UIItem> list;
    private View.OnClickListener onClickListener;

    public SimpleAdapter(Context context, List<UIItem> list) {
        this.context = context;
        this.list = list;
    }

    public void setClickListener(View.OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    @Override
    public int getItemViewType(int position) {
        return list.get(position).getType();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(context);
        RecyclerView.ViewHolder holder = null;
        switch(viewType) {
            case TYPE_1 :
                holder = new SimpleItemViewHolder1(inflater.inflate(R.layout.list_item_1, parent, false));
            break;
            case TYPE_2 :
                holder = new SimpleItemViewHolder2(inflater.inflate(R.layout.list_item_2, parent, false));
            break;
            default :
                holder = new SimpleItemViewHolder1(inflater.inflate(R.layout.list_item_1, parent, false));
        }

        return holder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        MenuItem item = (MenuItem) list.get(position).get();
        if( holder instanceof SimpleItemViewHolder1 ) {
            SimpleItemViewHolder1 holder1 = (SimpleItemViewHolder1) holder;
            holder1.listText1.setText(item.getItemName());
            holder1.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if( onClickListener != null ) {
                        onClickListener.onClick(v);
                    }
                }
            });
        } else if ( holder instanceof SimpleItemViewHolder2 ) {
            SimpleItemViewHolder2 holder2 = (SimpleItemViewHolder2) holder;
            holder2.listText2.setText(item.getItemName());
            holder2.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if( onClickListener != null ) {
                        onClickListener.onClick(v);
                    }
                }
            });
        }
    }

    class SimpleItemViewHolder1 extends RecyclerView.ViewHolder {

        private TextView listText1;

        SimpleItemViewHolder1(View view) {
            super(view);
            listText1 = view.findViewById(R.id.listText1);
        }
    }

    class SimpleItemViewHolder2 extends RecyclerView.ViewHolder {
        private TextView listText2;

        SimpleItemViewHolder2(View view) {
            super(view);
            listText2 = view.findViewById(R.id.listText1);
        }
    }
}

위의 자바 코드와 동일하게 동작되는 코드를 Kotlin Android Extensions를 활용하여 Kotlin 으로 작성한다면 아래와 같이 코드 양이 상당히 많이 감소됩니다.

class SimpleAdapter(private val context : Context, private val list: List<UIItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        const val TYPE_1    = 1
        const val TYPE_2    = 2
    }

    lateinit var onClickListener : View.OnClickListener

    override fun getItemCount(): Int = list.size

    override fun getItemViewType(position: Int): Int = list[position].type

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder =
        when(viewType) {
            TYPE_1 -> SimpleItemViewHolder1(context, parent)
            TYPE_2 -> SimpleItemViewHolder2(context, parent)
            else -> SimpleItemViewHolder1(context, parent)
        }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =
        (list[position].get() as MenuItem).let {
            if( holder is SimpleItemViewHolder1 ) {
                with( holder.itemView ) {
                    listText1.text = it.itemName
                    setOnClickListener { onClickListener?.onClick(this) }
                }
            } else if ( holder is SimpleItemViewHolder2 ) {
                with( holder.itemView ) {
                    listText2.text = it.itemName
                    setOnClickListener { onClickListener?.onClick(this) }
                }
            }
        }

    inner class SimpleItemViewHolder1(context : Context, parent : ViewGroup?) :
            RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(R.layout.list_item_1, parent, false))

    inner class SimpleItemViewHolder2(context : Context, parent : ViewGroup?) :
            RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(R.layout.list_item_2, parent, false))

간단한 텍스트뷰 하나를 갖고있는 ViewHolder 라고 하더라도 위와 같은 형태 코드를 샘플로 작성하였을 때 거의 반 정도의 코드양이 줄었습니다. 중요한 것은, 코드 양이 많이 줄었음에도 불구하고 다른 작업자가 해당 코드를 봤을 때도 모호하거나 코드가 축약된 느낌이 별로 없었다는 점이었습니다.

Android KTX

2018년 2월에는 안드로이드에서는 KTX를 발표했습니다. SDK 의 코드들이 Java 문법에 맞춰서 되어있는 것을 좀 더 Kotlin적으로 작성할 수 있는 확장 세트라고 소개하고 있습니다. 과제 작업 중 몇 가지 코드 작성 부분이 좀 더 코틀린 스럽게 다듬는데 유용했습니다. KTX 추가 이전에 Preference를 관리해주는 클래스의 특정 값을 읽어오는 코드는 아래와 같았습니다.

/** Guide is shown at first launch */
var isGuideShown : Boolean
    get() = preferences.getBoolean("is_guide_shown", false)
    set(value) = preferences.edit().putBoolean("is_guide_shown", value).apply()

KTX추가 이 후로 위 코드는 아래와 같이 변경되었습니다.

/** Guide is shown at first launch */
var isGuideShown : Boolean
    get() = preferences.getBoolean("is_guide_shown", false)
    set(value) = preferences.edit { putBoolean("is_guide_shown", value) }

Android KTX 추가 이후 작성되는 코드는 조금 더 Kotlin 스러워졌다고 생각합니다. 관련 내용의 상세 내용은 여기를 참조하시면 어떤것들이 있는지 훑어 보는데 도움이 될 것 같습니다. 향후 코드 작성 시 KTX 에서 제공되는 형태로 코드를 작성하려고 노력하고 있습니다.

이슈

Android 를 Kotlin 으로 작성하면서 겪었던 몇 가지 이슈사항을 정리해보고자 합니다.

Kotlin 를 Java 에서 호출할 때

Java 의 타입은 null 인지 non-null 구분을 하지 않지만, Kotlin 에서는 문법상 이것을 구분하고 있습니다. 모든 코드를 Kotlin 으로 변경하지 전까지는 각 개발자가 조금 더 신경 쓸 수밖에 없었습니다. Java 에서 Kotlin 코드로 작성된 함수를 호출할 때 parameter 가 null 이 가능할 지, non-null 일 지를 잘 고려하여 작성하면서 ?. , ?: , ! 등 null 처리 관련 연산자들을 잘 활용하여 회피하는 방법을 사용하였습니다.

Java 에서 Kotlin 을 호출할 때

Kotlin 으로 작성한 클래스에서 Static Field, Static Methods 를 제공하려면 companion object 를 사용해야 했습니다. 접근하려는 것이 상수값이라면 @JvmField, 함수라면 @JvmMethod 를 이용하면 됩니다. 하지만 kotlin 규칙 상 companion object 는 하나의 클래스에서 한 개의 companion object 만 존재하므로, 코드 작성이 어려움이 많았습니다. 따라서 Kotlin 코드 작성 시 가급적 java 에서 호출될 만한 부분은 최소화 하는 방향으로 논의하여 코드 정리를 할 수 밖에 없었습니다. 자바에서 코틀린을 사용하기 위한 다른 방법은 여기를 참조하세요.

Proguard 빌드 시 transformClassesAndResourcesWithProguard

Kotlin 으로 신나게 코드를 작성하고 나서 마지막으로 제품 릴리즈를 하려 하는데 transformClassesAndResourcesWithProguard*** 과 함께 빌드가 실패되는 경우가 있었습니다. 원인은 아래와 같은 코드에서 발생되었었습니다. submit 은 Button 객체입니다.

private fun initView() {
   with(submit) {
       setOnClickListener {
           val api = Api.aaa().get(inputl.text.toString())
           val success = Action1<ResponseContract.StringResponse> {
               setResult(RESULT_OK)
               finish()
           }

           val fail = Action1<Throwable> {
               Log.e(TAG, it)
           }

           requestApi(subscription, api, success, fail)
       }
   }
   ........
}

에러 로그 확인 결과 ‘~~~~~~$1$1fail$1 cant’find referenced class’ 라는 메시지가 출력되는데, 추측을 해보면 setOnClickListener scope 안에 it 으로 지칭될 녀석이 success 의 it 과 fail 의 it 두가지 인데, 프로가드로 빌드 할 때는 이 두가지의 타입 추론을 제대로 못하는 것으로 예상하고 있습니다.

private fun initView() {
   with(submit) {
       setOnClickListener {
           val api = Api.aaa().get(inputl.text.toString())
           val success = Action1<ResponseContract.StringResponse> {
               setResult(RESULT_OK)
               finish()
           }

           requestApi(subscription, api, success, null)
       }
   }
   ........
}

fail 을 제거하자 거짓말처럼 정상적으로 Proguard 빌드가 성공되었습니다. inline 으로 계속 코드를 작성하는데 재미가 들려서익숙해 지면서 점진적으로 깊이가 깊어지게 되었는데, 해당 이슈를 겪고나서 깊어지게 되는 케이스의 경우 해당부분을 좀 더 함수로 분리하는 방향으로 해결하려 노력하고 있습니다.


마치며

Kotlin으로 안드로이드 개발을 시작하지 않고 계시다면, Java 언어와 혼용하여 사용이 가능하니 간단한 클래스 하나부터 Kotlin 으로 작성하여 시작해 보는 것을 권장하고 싶습니다. 일단 시작하면 점진적으로 하나하나 알아가는 재미가 있기도 하지만, 무엇보다 안드로이드 개발을 위한 다양한 도구들이나 예제들도 점차 Kotlin 언어로 제공되기 시작했기 때문입니다. 이미 안드로이드 스튜디오에서 New 메뉴로 생성되는 코드들은 Java 에서 Kotlin 으로 변경되었습니다. 그리고Android Studio 의 다양한 기능(특히 자동완성)이 Kotlin 개발을 시작하는데 있어서 상당히 많은 도움이 되었습니다.

가장 큰 장점은 함께 안드로이드 개발을 하고 있는 여러 개발자분들과 다양한 의견을 교류하여 새로운 것을 알아가는 것이라고 생각합니다. 아직 안드로이드 개발을 Kotlin 으로 시작하지 않으셨다면 간단한 코드작성 부터 시작하는 것을 적극 추천하고 싶습니다.


참조