Navigation Component DeepLink로 이동시 Arguments 전달 편의성 개선하기
Action으로 이동하는 것처럼 DeepLink로 Arguments 전달하기
구현 목적
DeepLink 사용 시에도 Action으로 이동할 때 Arguments를 전달하는 것처럼 구현 하고자 합니다.
먼저 DeepLink와 Action으로 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를 따라서 이동합니다.
이를 통해 도착 Fragment에 Arguments를 보내려면, “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.xml에 Argument를 추가합니다.
- URI에 위와 같이 작성할 필요 없음
구현 과정
1. androidx.navigation.safeargs 동작 분석
safeargs의 내부 로직을 살펴보던 중, 도착 프래그먼트에서 by navArgs로 Arguments를 처리할 때
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의 클래스 명을 전달해야 합니다.
- val 과 NonNull로 속성을 만들어야 합니다.
- 이 부분이 상당한 제약 사항인데 추후 로직을 더 개선해서 업데이트 할 예정입니다.
2. navigateByDeepLink() 사용
// 출발 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()