저희 회사에서는 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
}
- 공통으로 사용할 수 있어야 한다 => Generic
- XXXFragmentArgs 를 만들어줘야 한다 => 객체 생성
- 원하는 함수를 호출해야 한다 => 메소드명을 알아야 한다.
- 호출한 함수의 리턴 값을 반환해야 한다 => 리턴 타입을 알아야 한다.
첫 번째로 인지해야 할 점은
XXXFragmentArgs.fromBundle(bundle) 을 통해
객체를 생성한다는 것.
그럼 위의 메소드를 어떻게 실행할까? 코드를 살펴보자.
// 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 하게 값을 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
- 위의 방식으로 NavArgs 클래스 추출
- 키의 값으로 호출하는 메소드를 찾는다.
- 찾은 메소드를 호출하여 값을 추출
❗️ kotlin reflection 을 사용해서 구현 했습니다.
이유는 NavArgs 를 KClass로 받아오는김에 + 사용 편리함으로
kotlin reflection 으로 처리 했습니다.
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
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 parseKeyToUpperFirstIndex(key: String) = key
.mapIndexed { index, c ->
if (index == 0) {
c.uppercaseChar()
} else {
c
}
}.join("")
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)
}
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)
}
}
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 했고
팀원들이 자주 사용하는지 지켜본 후에
개선할 수 있으면
개선 시켜볼려고 합니다 :)
피드백은 언제든 환영이며
도움이 되시길 바라며 🙏
이만 마치겠습니다 :)