[Navigation] Reflection 과 Delegated Property를 이용한 SafeArgs 데이터 가져오기

David·2022년 7월 16일
0

🤔 Step을 줄이고 싶소


저희 회사에서는 fragment 에서 데이터를 가져올 시
다음 코드와 같이 가져옵니다.

// first-step
private val args: MessageListFragmentArgs by lazy {
    MessageListFragmentArgs.fromBundle(arguments)
}

// second-step 
val talentId: String? by lazy {
    args.talentId
}

val talk: Talk? by lazy {
    args.talk
}

격하게 하나의 step으로 수정하고 싶다는 생각이 들었습니다.

drawing

1Step으로 하기 위해 생각해 봐야할 점

  • 공통으로 사용할 수 있어야 한다 => Generic
  • XXXFragmentArgs 를 만들어줘야 한다 => 객체 생성
  • 원하는 함수를 호출해야 한다 => 메소드명을 알아야 한다.
  • 호출한 함수의 리턴 값을 반환해야 한다 => 리턴 타입을 알아야 한다.



⏳ Lazy Delegated Property ...?!


💭 요런식으로 뽑아 오고 싶다


🧩 navigation-ktx 으로 부터 영감을... > 분석 해보자!

첫 번째로 인지해야 할 점은
XXXFragmentArgs.fromBundle(bundle) 을 통해
객체를 생성한다는 것.

그럼 위의 메소드를 어떻게 실행할까? 코드를 살펴보자.

  • Java Reflection 지식 필요

// step-1: XXXFragmentArgs 타입의 Genric 클래스 받아오기 
private val navArgsClass: KClass<Args>


// step-2: XXXFragmentArgs 의 메소드중에서 fromBundle 명의 Method 찾기
val method: Method = navArgsClass.java.getMethod("fromBundle", *methodSignature)


// step-3: XXXFragmentArgs.fromBundle 호출하여 > XXXFragmentArgs 반환
method.invoke(null, arguments) as Args

여기까지 코드가 이해됐다면
Lazy 하게 가져오는 코드가 이해될 것 입니다.

Lazy< T > 로 가져오기

Lazy 하게 값을 Delegate 하기 위해 Lazy<T> 를 구현해야 합니다.

class Clz<T>(): Lazy<T> {
	override val value:T
    override fun isInitialized()
}

class NavArgsLazy<Args : NavArgs>(
    private val navArgsClass: KClass<Args>,
    private val argumentProducer: () -> Bundle
) : Lazy<Args> {
    private var cached: Args? = null

    override val value: Args
        get() {
            var args = cached
            if (args == null) {
                val arguments = argumentProducer()
                val method: Method = methodMap[navArgsClass]
                    ?: navArgsClass.java.getMethod("fromBundle", *methodSignature).also { method ->
                        // Save a reference to the method
                        methodMap[navArgsClass] = method
                    }

                @Suppress("UNCHECKED_CAST")
                args = method.invoke(null, arguments) as Args
                cached = args
            }
            return args
        }

    override fun isInitialized() = cached != null



🪝 원하는 값을 뽑아보자


다음 절차로 원하는 값을 추출할 것이다.

  1. 위의 방식으로 NavArgs 클래스 추출
  2. 키의 값으로 호출하는 메소드를 찾는다.
  3. 찾은 메소드를 호출하여 값을 추출

❗️ kotlin reflection 을 사용해서 구현 했습니다.
이유는 NavArgs 를 KClass로 받아오는김에 + 사용 편리함으로
kotlin reflection 으로 처리 했습니다.

implementation - kotlin reflection

implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

- navArgs 추출

private fun createClassFragmentArgsInstance(arguments: Bundle): NavArgs {
        val method = navArgClass.java.getMethod("fromBundle", *signature)
        @Suppress("UNCHECKED_CAST")
        return method.invoke(null, arguments) as NavArgs
}

- key 첫 번째 index를 대문자로 파싱

private fun parseKeyToUpperFirstIndex(key: String) = key
        .mapIndexed { index, c ->
            if (index == 0) {
                c.uppercaseChar()
            } else {
                c
            }
        }.join("")

- method 호출 후 값 반환

private fun NavArgs.callMethod(
        methodName: String,
        findMethod: (String)-> KFunction<*>,
        returnValue: (R) -> Unit
    ) {
        findMethod(methodName)

        @Suppress("UNCHECKED_CAST")
        val result = findMethod.invoke(methodName).call(this) as R
        AppLogger.d("getArgument", "$methodName return value: $result")

        returnValue(result)
    }

- LazyNavArgsValue 클래스 전체 코드

class LazyNavArgsValue<R: Any?>(
    private val navArgClass: KClass<out NavArgs>,
    private val key: String,
    private val producer: () -> Bundle
) : Lazy<R?> {
    private var cached: R? = null

    override val value: R?
        get() {
            var args = cached
            if (args == null) {
                // Ex. MessageListFragmentArgs
                val fragmentArgsInstance = createClassFragmentArgsInstance(arguments = producer())
				
                val newKey = parseKeyToUpperFirstIndex(key)

                // Ex. MessageListFragmentArgs.getTalentId()
                fragmentArgsInstance
                    .callMethod(
                        methodName = "get".plus(newKey),
                        findMethod = { findMethodName ->
                            navArgClass
                                .functions
                                .firstOrNull {
                                    it.name == findMethodName
                                }
                                ?: throw IllegalStateException("safeArgs key is not found")
                        }
                    ) { returnObj ->
                        args = returnObj
                        cached = args
                    }

            }
            return args
        }

    override fun isInitialized(): Boolean = cached != null

    private fun parseKeyToUpperFirstIndex(key: String) = key
        .mapIndexed { index, c ->
            if (index == 0) {
                c.uppercaseChar()
            } else {
                c
            }
        }.join("")

    private fun createClassFragmentArgsInstance(arguments: Bundle): NavArgs {
        val method = navArgClass.java.getMethod("fromBundle", *signature)
        @Suppress("UNCHECKED_CAST")
        return method.invoke(null, arguments) as NavArgs
    }

    private fun NavArgs.callMethod(
        methodName: String,
        findMethod: (String)-> KFunction<*>,
        returnValue: (R) -> Unit
    ) {
        findMethod(methodName)

        @Suppress("UNCHECKED_CAST")
        val result = findMethod.invoke(methodName).call(this) as R
        AppLogger.d("getArgument", "$methodName return value: $result")

        returnValue(result)
    }

}

- getArgumentExtra

inline fun <R: Any?, reified Args: NavArgs> Presenter.getArgumentExtra(key: String) = LazyNavArgsValue<R>(
    Args::class,
    key
) {
    fragment?.arguments ?: throw IllegalStateException("Presenter $this has null arguments")
}

- 사용하기



👏🏻 포스트를 마치며


기존 사용 방식 2step > 1step 으로
자바와 코틀린 reflection 으로 처리를 해보았는데요.
많은 우여곡절 끝에 해내긴 했습니다 🤣
(특히 메소드 call 이 안 되어서 FragmentArgument Generated java code 분석하고
코틀린 reflection 샘플 코드 만들어서 테스트도 해본..)

해당 코드를 PR 했고
팀원들이 자주 사용하는지 지켜본 후에
개선할 수 있으면
개선 시켜볼려고 합니다 :)

피드백은 언제든 환영이며
도움이 되시길 바라며 🙏
이만 마치겠습니다 :)

profile
공부하는 개발자

0개의 댓글

관련 채용 정보