구현 목적

DeepLink 사용 시에도 Action으로 이동할 때 Arguments를 전달하는 것처럼 구현 하고자 합니다.

먼저 DeepLinkAction으로 Arguments를 전달하는 방법을 알아봅시다.

각각의 Arguments 전달 방법

Action으로 Arguments 전달

XML 네비게이션 파일에 Argument를 선언하거나, 동적으로 Arguments를 선언하는 방법이 있습니다.

// nav.xml 에서, 전달할 Arguments를 아래와 같이 정의해야 합니다.

<argument
    android:name="name"
    android:defaultValue="이름"/>

<argument
    android:name="age"
    android:defaultValue="나이"/>

// 출발 Fragment에서

val name = "이름"
val age = "5"

val action = StartFragmentDirections.userInfoAction(name, age)
v.findNavController().navigate(action)

// 도착 Fragment에서
val args: userInfoFragmentArgs by navArgs()

DeepLink로 Arguments 전달하는 방법

// nav.xml 에서, 전달할 Arguments를 아래와 같이 정의해야 합니다.

<argument
    android:name="name"
    android:defaultValue="이름"/>

<argument
    android:name="age"
    android:defaultValue="나이"/>

<deepLink app:uri="appname://app/user/userinfo?name={name}&age={age}" />

// 출발 Fragment에서
val name = "이름"
val age = "5"

"appname://app/user/userinfo?name=$name&age=$age".toUri()

// 도착 Fragment에서
val args: userInfoFragmentArgs by navArgs()

DeepLink로 이동할 때는 “appname://app/user/userinfo”.toUri() 를 사용해서 URI를 따라서 이동합니다.

이를 통해 도착 FragmentArguments를 보내려면, “appname://app/user/userinfo?name=name_data&age=age_data”.toUri() 과 같이 URI에 데이터를 직접 입력해서 전달해야 합니다.(UriBuilder를 통해서 Query를 입력해서 하는 방법도 있습니다)

  • 구현한 기능으로 DeepLink Arguments 전달 시에는
    • URI에 위와 같이 작성할 필요 없음
      • “appname://app/user/userinfo” 만 작성하면 됩니다
    • nav.xml에 Argument를 선언할 필요없음
      • BaseNavArgs를 상속받는 data class를 만들기만 하면됩니다.
      • data class를 만들면 동적으로 nav.xmlArgument를 추가합니다.

구현 과정

1. androidx.navigation.safeargs 동작 분석

safeargs의 내부 로직을 살펴보던 중, 도착 프래그먼트에서 by navArgsArguments를 처리할 때 fromBundle() static 메서드를 호출하여 Argument 객체에 담아 반환하는 로직을 발견하였다.

fromBundle()을 내부적으로 사용할때 이 메서드는 NavArgs 인터페이스를 구현하는 클래스의 static 메서드 여야 하는 것을 파악하였고, 이 두 조건을 만족하는 추상 클래스를 생성하였다.

abstract class BaseNavArgs(
    val className: String
) : NavArgs {

    @Suppress("CAST_NEVER_SUCCEEDS")
    fun toBundle(): Bundle {
        val result = Bundle()
        toMap().forEach { (key, value) ->
            result.putString(key, value)
        }
        return result
    }

    companion object {
        @JvmStatic
        fun fromBundle(bundle: Bundle): BaseNavArgs {
            // BaseNavArgs를 구현한 class를 className 문자열을 바탕으로 만든다.
            val kClass: KClass<BaseNavArgs> = Class.forName(bundle.getString("className")!!).kotlin as KClass<BaseNavArgs>
            bundle.classLoader = kClass.java.classLoader
            
            // 생성한 class의 생성자를 가져온다.
            val constructor = kClass.primaryConstructor!!

            // bundle의 value를 읽는다.
            val args = constructor.parameters.map { parameter ->
                bundle.getString(parameter.name, "")
            }

            // 읽은 value를 생성자에 담아서 class instance를 만든다.
            return constructor.call(*args.toTypedArray())
        }
    }

    fun toMap(): Map<String, String> = this::class.memberProperties.let { properties ->
        properties.associate { property ->
            property.name to property.getter.call(this).toString()
        }.toMap()
    }
}

toMap() 메서드는 BaseNavArgs를 구현하는 객체의 속성 값(Arguments)을 Map으로 반환하는 메서드이다.

2. 동적으로 Argument 생성하고, URI를 생성

위 BaseNavArgs를 구현하는 data class를 바탕으로 동적으로 Argument를 생성한다.

  • navigateByDeepLink(deepLinkUrl: String, parameter: BaseNavArgs)
    • DeepLink룰 사용할 때, 해당 URI가 선언된 NavGraph에 동적으로 Arguments를 추가
    • @param deepLinkUrl DeepLink Url
    • @param parameter DeepLink에 들어갈 파라미터, BaseNavArgs를 구현한 클래스 객체
fun NavController.navigateByDeepLink(deepLinkUrl: String, parameter: BaseNavArgs) {
    val parameterMap = parameter.toMap()
    toDeepUrl(deepLinkUrl, parameterMap).also { finalUri ->
        graph.matchDeepLink(NavDeepLinkRequest(finalUri, null, null))?.also { deepLinkMatch ->
            parameterMap.takeIf {
                it.isNotEmpty()
            }?.forEach { (key, value) ->
                deepLinkMatch.destination.addArgument(
                    key, NavArgument.Builder().setType(NavType.StringType).setIsNullable(false).setDefaultValue(value).build()
                )
            }
        }
    }

    this.navigate(deepLinkUrl.toUri())
}

아래의 toDeepUri()를 사용하여 URI를 생성한다.

동적으로 생성된 Arguments를 appname://app/user/userinfo?name=name_data&age=age_data과 같이 GET URI로 만든다.

Map을 메서드의 매개변수로 받아서 Map의 key, value를 URI에 추가한다.

/**
 * Uri Builder
 *
 * Uri를 생성하는 함수입니다.
 *
 * @param parameter Uri에 들어갈 파라미터
 * @return Uri
 */
private fun toDeepUrl(deepLinkUrl: String, parameter: Map<String, String>): Uri = StringBuilder(deepLinkUrl).let { uri ->
    parameter.takeIf {
        it.isNotEmpty()
    }?.also { map ->
        uri.append("?")
        map.onEachIndexed { index, entry ->
            uri.append("${entry.key}=${entry.value}")
            if (index != map.size - 1) uri.append("&")
        }
    }
    uri.toString().toUri()
}

사용 방법

1. 전달할 Arguments data class 생성

/**
 * 유저 정보 프래그먼트로 전달할 인자
 *
 * @property name 이름
 * @property age 나이
 */
data class UserInfoFragmentArgs(
    val name: String, val age: String
) : BaseNavArgs(UserInfoFragmentArgs::class.java.name)

주의사항

  • 상속받는 data class를 만들었을때 BaseNavArgs 의 생성자에 UserInfoFragmentArgs::class.java.name 과 같이 data class의 클래스 명을 전달해야 합니다.
  • valNonNull로 속성을 만들어야 합니다.
    • 이 부분이 상당한 제약 사항인데 추후 로직을 더 개선해서 업데이트 할 예정입니다.
// 출발 Fragment에서

activity?.findNavController(com.android.appname.core.common.R.id.fragmentContainerView)?
.navigateByDeepLink(
    "appname://app/user/userinfo",
    UserInfoFragmentArgs(
        name = userInfoDto.name,
        age = userInfoDto.age
    )
)

// 도착 Fragment에서
val args: userInfoFragmentArgs by navArgs()