Java to Kotlin – Improve Interface to function (listener pattern)

몇일 전, Kotlin – SAM Conversions 라는 글을 올렸었는데 몇일 생각해보면서 Kotlin – Functions in java 에 나온 것 처럼 function 에 이관시키는게 좀 더 좋을거라 판단했다.

그래서, 리스너 패턴에 사용되던 자바의 인터페이스를 코틀린의 function로 변환하는 과정을 정리해보려 한다.

Step -> { Interface to function }

1. 변환할 Interface 의 구조 알아보기

interface PermissionRequestCallback {
    fun onPermissionResult(resultCode: Int, list: ArrayList<String>)
}

Convert java into kotlin 변환기 사용해서 생성한 인터페이스이다. 하나의 메소드를 가지고 있으며 resultCode: Int, list: ArrayList<String> 의 두개의 파라미터를 가지고 있다.

2. 파라미터 수정

기존에 checkPermission은 fun checkPermission(list: ArrayList<String>, callback: PermissionRequestCallback? = null): Boolean 이런 구조였으나 인터페이스를 Function으로 교체하기 위해 callback: (Int, ArrayList<String>) -> Unit 로 교체해주었다.
두 개의 파라미터를 가지면 순서대로 해당 변수의 타입명을 적어주고, 리턴값은 없어도 되니 Unit를 사용한다. 기본값은 설정하지 않았다. (보통, 권한 요청하고 받으면 바로 작업을 실행하게 만들기 때문이다.)

기본값을 추가시키게 되면 형태는 callback: (Int, ArrayList<String>) -> Unit = {} 가 된다.

만일 파라미터가 하나일 경우 callback: Int.() -> Unit 라고 해도 무방하다.

최종적으로 수정된 checkPermission은 아래와 같다.

 /**
  * check and request Permission which given.
  *
  * @param[array] array of Permission to check
  * @param[callback] callback object
  * @return check result
  */
fun checkPermission(array: Array<String>, callback: (Int, ArrayList<String>) -> Unit): Boolean {
    val permissionList: ArrayList<String> = ArrayList()
    array.forEach { permissionList.add(it) }

    return checkPermission(permissionList, callback)
}

기타 checkPermission, requestPermission 등도 위와 같이 수정했다.

3. callback 파라미터 사용

사용할때는 비교적 간단하다. 기존에는 callback?.onPermissionResult(PERMISSION_ALREADY, list) 이런 식으로 사용했었는데, callback(PERMISSION_ALREADY, list) 로 바꿔주면 된다.

4. 실제로 Kotlin에 사용해보자.

Before

class Callback : RPermission.PermissionRequestCallback {
    override fun onPermissionResult(resultCode: Int, list: ArrayList<String>) {
         alert(message = "Permission result -> $resultCode / Requested Permission: ${TextUtils.join(",", list)}")
    }
}

btnCall.setOnClickListener {
    val arrays : Array<String> = arrayOf(Manifest.permission.CALL_PHONE)
    RPermission.getInstance(this).checkPermission(arrays, Callback())
}

After

btnCall.setOnClickListener {
    val arrays: Array<String> = arrayOf(Manifest.permission.CALL_PHONE)
    RPermission.getInstance(this).checkPermission(array = arrays, callback = { resultCode: Int, list: ArrayList<String> ->
        alert(message = "Permission result -> $resultCode / Requested Permission: ${TextUtils.join(",", list)}")
    })
}

코드도 더 짧아졌고, 무엇보다 class를 선언하지 않아도 된다는게 아주 좋아졌다.

5. 그럼 자바는?

btnCall.setOnClickListener(view -> {
    String[] arrays = new String[]{Manifest.permission.CALL_PHONE};

    RPermission.Companion.getInstance(this).checkPermission(arrays, (integer, strings) -> {
        Utils.alert(PermissionActivity.this,
                    "Permission result ->" + integer + " / Requested Permission: " + TextUtils.join(",", strings));
        return Unit.INSTANCE;
    });
});

람다식을 사용하게 되면 코틀린과 거의 비슷하게 사용할 수 있다.

사용하지 않게 되면..

btnCall.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String[] arrays = new String[]{Manifest.permission.CALL_PHONE};

        RPermission.Companion.getInstance(PermissionActivity.this).checkPermission(arrays, new Function2<Integer, ArrayList<String>, Unit>() {
             @Override
             public Unit invoke(Integer integer, ArrayList<String> strings) {
                    Utils.alert(PermissionActivity.this,
                               "Permission result ->" + integer + " / Requested Permission: " + TextUtils.join(",", strings));
                    return Unit.INSTANCE;
             }
        });
    }
});

결론

람다를 사용하지 않게 되는 경우는 제외하고, 단순히 리스너 패턴으로 사용했던 인터페이스는 function으로 갈아타는게 좀 더 가독성도 있을 뿐더러 코드가 짧아진다는 결론에 도착하였다.

이제 남은건, 다른 클래스들도 이렇게 변환하는 것이다.

Android – Exit Applicaction with finishAffinity

exit sign

 

기본적으로 안드로이드 앱을 종료하는 법은 finish()이다. 구글에서 가장 권장하는 방법이기도 하고. 하지만 스택 관리가 잘 안되거나 다른 버그가 발생하면 분명히 finish() 해도 다른 액티비티가 “뿅!” 하고 나오는 일이 잦다.

물론 finish 외에도 android.os.Process.killProcess(android.os.Process.myPid()) 나 System.exit(0) 등이 있기야 하지만 다소 안드로이드에 안맞긴 한다. 결과적으로 위 두개 다 다른 액티비티가 나오는 등의 버그는 일어난다.

 Solutions – finishAffinity()

그래서 API 16부터 추가된게 Activity.finishAffinity() 이다. 어느 액티비티에서나 상위 스택에 쌓여진 액티비티를 종료할 수 있다.  API 16보다 아래의 버전을 지원하는 경우 ActivityCompat.finishAffinity() 를 사용하면 된다.

finishAffinity에는 또 다른 사용법이 있는데 바로 앱을 재부팅 하는 것이다. 서비스등만 따로 종료하면 문제 없이 작동한다. 아래는 개발중인 RichUtils 라이브러리에 있는 코틀린 버전의 재부팅 메소드이다. (원본 코드: RichUtilsKt/RReboot.kt) CLEAR_TOP 로 홈 액티비티나 지정한 액티비티로 이동하고 finishAffinity()로 부모 액티비티를 날려버리는 구조이다.

@file:JvmName("Utils")
@file:JvmMultifileClass

package pyxis.uzuki.live.richutilskt

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build

/**
 * Reboot application
 *
 * @param[restartIntent] optional, desire activity for reboot
 */
@JvmOverloads fun Context.reboot(restartIntent: Intent = this.packageManager.getLaunchIntentForPackage(this.packageName)) {
    restartIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
    if (this is Activity) {
        this.startActivity(restartIntent)
        finishAffinity(this)
    } else {
        restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        this.startActivity(restartIntent)
    }
}

private fun finishAffinity(activity: Activity) {
    activity.setResult(Activity.RESULT_CANCELED)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        activity.finishAffinity()
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        activity.runOnUiThread { activity.finishAffinity() }
    }
}

다만 이 finishAffinity 또한 앱 종료를 위한 최적의 방법은 아니다. 안드로이드엔 앱 종료라는 개념이 존재하지 않기 때문이다. 생명주기만 존재할 뿐이다. 단지 모든 부모 액티비티를 종료하기에 안드로이드 VM이 메모리에서 제거하는 뿐이다.

또, 서비스는 따로 종료해야 하기에 정확히는 종료한다고 보긴 어렵다.

 Solution – finishAndRemoveTask()

API 21부터 추가된 Activity.finishAndRemoveTask() 는 종료함과 동시에 테스크 바(최근 내역)에서 지운다. 검색해보니 크로니움 코드의 ApiCompatibilityUtils 에 호환 코드 비슷한게 있다.

/**
 * @see android.app.Activity#finishAndRemoveTask()
 */
public static void finishAndRemoveTask(Activity activity) {
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
        activity.finishAndRemoveTask();
    } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
        // crbug.com/395772 : Fallback for Activity.finishAndRemoveTask() failing.
        new FinishAndRemoveTaskWithRetry(activity).run();
    } else {
        activity.finish();
    }
}

private static class FinishAndRemoveTaskWithRetry implements Runnable {
    private static final long RETRY_DELAY_MS = 500;
    private static final long MAX_TRY_COUNT = 3;
    private final Activity mActivity;
    private int mTryCount;
    FinishAndRemoveTaskWithRetry(Activity activity) {
        mActivity = activity;
    }
    @Override
    public void run() {
        mActivity.finishAndRemoveTask();
        mTryCount++;
        if (!mActivity.isFinishing()) {
            if (mTryCount < MAX_TRY_COUNT) {
                ThreadUtils.postOnUiThreadDelayed(this, RETRY_DELAY_MS);
            } else {
                mActivity.finish();
            }
        }
    }
}

다만 이거도 문제가 있는게, 다시 호출하면 어째선지 Application의 onCreate()가 불리지 않아 사용을 포기해야만 했었다.

 고전 방법

그닥 신경쓰지 않아도 되는 코드면 아래와 같이 해도 무방하긴 하다.

System.runFinalizersOnExit(true);
System.exit(0);

 So…

사실 안드로이드 앱 개발자에게 ‘어떻게 앱을 종료시킬 것인가’ 는 오래전부터 연구된 문제긴 하다. killProcess, exit, runFinalizersOnExit 등… 안드로이드의 컨셉 자체가 위에서도 말했듯이 앱 종료라는 개념이 없다. 이 때 개발자가 할 수 있는건 좀 더 빨리 heap memory 에서 사라지게 하는 것 뿐인거 같다.

물론, finish() 로도 종료할 수 있게 앱의 액티비티 스택을 잘 관리하는게 Best지만..;-;

어느정도 시행착오 해본 결과 finishAffinity 가 그나마 안드로이드 환경에 맞게 ‘사실상’으로 앱을 종료하는 것 같다. 서비스는 따로 종료시켜야 되면서도 이제까지는 잘 되었기 때문이다. 앞으로 더 나은 메소드가 나와 한방에 정리되었으면 하는 바람이 있다.

새로운 포스트 시리즈 – Advanced Kotlin Programming

소개

Kotlin이 안드로이드 주 언어가 된지 1개월 정도 지났습니다. 그래서 틈틈히 코틀린을 공부하면서 인터넷에 JetBrains에 일하고 계신 Hadi Hariri님의 강의를 찾아 공부할 예정입니다.
O’Reillt Media 에서 내놓은 강의로 러닝타임 3시간의 영상 강의로 제목은 ‘Advanced Kotlin Programming From Nested Functions to Asynchronous Programming’ 입니다. 한글로 번역하자면 ‘고급 Kotlin 프로그래밍, 중첩 함수에서 비동기 프로그래밍에 이르기까지‘ 입니다.

주로 강의에 대한 내용을 메모, 차후 설명을 덧붙일 예정입니다.

강의 목적

  • Kotlin을 다른 JVM와 비교하여 확장 가능하고 독창적인 접근법을 이해한다.
  • 중위(infix)함수, Tail 재귀 및 람다 확당과 같은 고급 주제 검토
  • 클래스를 위해 제공하는 창의적 기능에 대한 탐색
  • Delegation에 대한 것을 코틀린이 어떻게 컨트롤 하는지
  • 제너릭 관련 문제에 대한 심층적인 연구 – 제약조건, 공분산 등
  • 코틀린이 메타 프로그래밍 및 introspection을 위해 사용하는 기술
  • 코루틴을 이용한 비동기 프로그래밍

강의는 http://shop.oreilly.com/product/0636920052999.do 에서 볼 수 있습니다.
이번 기회로 코틀린에 대해 더 자세히 알고 실전에서 사용할 수 있으면 좋겠다고 생각합니다.