Advanced Kotlin – Local Functions

Local Functions

코틀린 메소드는 파일 최상단에 위치할 수 있다.

즉 메소드를 가지고 있기 위해 클래스를 생성해야 되는 자바, C#, 스칼라와 달리 생성하지 않아도 된다.

추가적으로 코틀린은 하위 메소드나 확장 메소드로써 로컬 메소드를 가질 수 있다.

local functions code

local functions code

로컬 메소드는 메소드 안의 메소드로, 로컬 메소드는 부모 메소드(outer functions)의 파라미터, 지역변수에 접근이 가능하다.

코드 리뷰나 테스트를 위해 코드를 작성할 때에 유용하게 사용할 수 있다. 그리고 당연하게도 다른 메소드에서는 접근을 하지 못한다.

제일 중요한건 부모 메소드의 ‘지역변수’ 에도 접근할 수 있기 때문에 굳이 outerFunction을 전역변수로 두지 않아도 되는 점이다. 코틀린의 장점이 드러나는 부분이라고 생각했다.

해당 기능의 Documents: https://kotlinlang.org/docs/reference/functions.html#function-scope

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 가 그나마 안드로이드 환경에 맞게 ‘사실상’으로 앱을 종료하는 것 같다. 서비스는 따로 종료시켜야 되면서도 이제까지는 잘 되었기 때문이다. 앞으로 더 나은 메소드가 나와 한방에 정리되었으면 하는 바람이 있다.