From 9d130af45afb5dd23fa7967d538c8b19992fb128 Mon Sep 17 00:00:00 2001 From: JSPark <48265129+pknujsp@users.noreply.github.com> Date: Thu, 1 Jun 2023 15:00:16 +0900 Subject: [PATCH] =?UTF-8?q?#91=20navigateByDeepLink=20=EA=B0=9C=EC=84=A0,?= =?UTF-8?q?=20arguments=EB=A5=BC=20=EC=A0=84=EB=8B=AC=EB=B0=9B=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=EA=B2=BD=EC=9A=B0=EC=97=90=20=EB=B9=88=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/common/util/UriBuilder.kt | 89 +++++++++++++++++-- .../core/model/local/navargs/BaseNavArgs.kt | 44 ++++++++- .../RecallSuspensionDataSourceImpl.kt | 3 +- .../mediproject/feature/news/NewsFragment.kt | 15 ++-- .../mediproject/feature/news/NewsScreen.kt | 8 +- .../RecentPenaltyListFragment.kt | 10 ++- 6 files changed, 139 insertions(+), 30 deletions(-) diff --git a/core/common/src/main/java/com/android/mediproject/core/common/util/UriBuilder.kt b/core/common/src/main/java/com/android/mediproject/core/common/util/UriBuilder.kt index 5952e218d..e80ac4690 100644 --- a/core/common/src/main/java/com/android/mediproject/core/common/util/UriBuilder.kt +++ b/core/common/src/main/java/com/android/mediproject/core/common/util/UriBuilder.kt @@ -1,10 +1,16 @@ package com.android.mediproject.core.common.util import android.net.Uri +import android.os.Bundle +import androidx.annotation.MainThread import androidx.core.net.toUri +import androidx.fragment.app.Fragment +import androidx.navigation.NavArgsLazy import androidx.navigation.NavArgument import androidx.navigation.NavController +import androidx.navigation.NavDeepLink import androidx.navigation.NavDeepLinkRequest +import androidx.navigation.NavOptions import androidx.navigation.NavType import com.android.mediproject.core.model.local.navargs.BaseNavArgs @@ -16,7 +22,7 @@ import com.android.mediproject.core.model.local.navargs.BaseNavArgs * @param parameter Uri에 들어갈 파라미터 * @return Uri */ -private fun toDeepUrl(deepLinkUrl: String, parameter: Map): Uri = StringBuilder(deepLinkUrl).let { uri -> +private fun toQueryUri(deepLinkUrl: String, parameter: Map): Uri = StringBuilder(deepLinkUrl).let { uri -> parameter.takeIf { it.isNotEmpty() }?.also { map -> @@ -31,6 +37,29 @@ private fun toDeepUrl(deepLinkUrl: String, parameter: Map): Uri = uri.toString().toUri() } +/** + * Uri Builder + * + * 쿼리가 key만 포함된 Uri를 생성하는 함수입니다. + * + * @param parameter Uri에 들어갈 파라미터 + * @return Uri + */ +private fun toBaseUri(deepLinkUrl: String, parameter: Map): Uri = StringBuilder(deepLinkUrl).let { uri -> + parameter.takeIf { + it.isNotEmpty() + }?.also { map -> + uri.append("?") + map.onEachIndexed { index, entry -> + if (entry.value != null) { + uri.append("${entry.key}={${entry.key}}") + if (index != map.size - 1) uri.append('&') + } + } + } + uri.toString().toUri() +} + /** * NavController를 확장하는 함수입니다. * @@ -39,19 +68,63 @@ private fun toDeepUrl(deepLinkUrl: String, parameter: Map): Uri = * @param deepLinkUrl DeepLink Url * @param parameter DeepLink에 들어갈 파라미터 */ -fun NavController.navigateByDeepLink(deepLinkUrl: String, parameter: BaseNavArgs) { +fun NavController.navigateByDeepLink( + deepLinkUrl: String, parameter: BaseNavArgs, navOptions: NavOptions? = null) { val parameterMap = parameter.toMap() - toDeepUrl(deepLinkUrl, parameterMap).also { finalUri -> + toQueryUri(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() - ) + + value ?: return@forEach + + val navType: NavType = when (value) { + is String -> NavType.StringType + is Int -> NavType.IntType + is Long -> NavType.LongType + is Float -> NavType.FloatType + is Boolean -> NavType.BoolType + else -> NavType.ReferenceType + } + + deepLinkMatch.destination.addArgument(key, + NavArgument.Builder().setType(navType).setIsNullable(true).setDefaultValue(value).build()) } } } - this.navigate(deepLinkUrl.toUri()) -} \ No newline at end of file + this.navigate(deepLinkUrl.toUri(), navOptions) +} + + +fun NavController.deepNavigate( + deepLinkUrl: String, parameter: BaseNavArgs, navOptions: NavOptions? = null) { + val parameterMap = parameter.toMap() + + // "medilenz://main?name={name}&age={age}" + val baseUri = toBaseUri(deepLinkUrl, parameterMap) + // "medilenz://main?name=NAME&age=AGE" + val finalUri = toQueryUri(deepLinkUrl, parameterMap) + + graph.matchDeepLink(NavDeepLinkRequest(baseUri, null, null))?.apply { + // finalUri를 도착지 그래프에 새로운 딥링크로 추가 + if (!destination.arguments.contains(NAV_KEY)) { + destination.addArgument(NAV_KEY, NAV_ARG) + destination.addDeepLink(NavDeepLink.Builder().setUriPattern(baseUri.toString()).build()) + } + } + + navigate(finalUri, navOptions) +} + +@MainThread +inline fun Fragment.navArgs(): NavArgsLazy = NavArgsLazy(Args::class) { + val bundle = arguments ?: Bundle() + bundle.apply { + putString("className", Args::class.java.name) + } +} + +const val NAV_KEY = "__Deep_Args_" +private val NAV_ARG = NavArgument.Builder().setType(NavType.BoolType).setDefaultValue(true).build() \ No newline at end of file diff --git a/core/model/src/main/java/com/android/mediproject/core/model/local/navargs/BaseNavArgs.kt b/core/model/src/main/java/com/android/mediproject/core/model/local/navargs/BaseNavArgs.kt index 8105bc8e6..1c9c16f49 100644 --- a/core/model/src/main/java/com/android/mediproject/core/model/local/navargs/BaseNavArgs.kt +++ b/core/model/src/main/java/com/android/mediproject/core/model/local/navargs/BaseNavArgs.kt @@ -5,11 +5,11 @@ import androidx.navigation.NavArgs import kotlin.reflect.KClass import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.full.starProjectedType import kotlin.reflect.javaType abstract class BaseNavArgs( - val className: String -) : NavArgs { + val className: String) : NavArgs { fun toBundle(): Bundle { val result = Bundle() toMap().forEach { (key, value) -> @@ -25,10 +25,27 @@ abstract class BaseNavArgs( return result } + fun toBundle(map: Map): Bundle { + val result = Bundle() + map.forEach { (key, value) -> + when (value) { + null -> return@forEach + is String -> result.putString(key, value) + is Int -> result.putInt(key, value) + is Long -> result.putLong(key, value) + is Float -> result.putFloat(key, value) + is Boolean -> result.putBoolean(key, value) + } + } + return result + } + companion object { @OptIn(ExperimentalStdlibApi::class) @JvmStatic fun fromBundle(bundle: Bundle): BaseNavArgs { + if (bundle.containsKey("className") && bundle.size() == 1) return empty(bundle.getString("className")!!) + val kClass: KClass = Class.forName(bundle.getString("className")!!).kotlin as KClass bundle.classLoader = kClass.java.classLoader @@ -42,12 +59,33 @@ abstract class BaseNavArgs( Long::class.java -> bundle.getLong(parameter.name) Float::class.java -> bundle.getFloat(parameter.name) Boolean::class.java -> bundle.getBoolean(parameter.name) - else -> throw IllegalArgumentException("Argument \"${parameter.name}\" is marked as non-null but was passed a null value.") + else -> {} } } } return constructor.call(*args.toTypedArray()) } + + @JvmStatic + private fun empty(className: String): BaseNavArgs { + val kClass: KClass = Class.forName(className).kotlin as KClass + + val constructor = kClass.primaryConstructor!! + + val args: List = constructor.parameters.map { kProperty1 -> + when (kProperty1.type) { + String::class.starProjectedType -> "" + Int::class.starProjectedType -> 0 + Long::class.starProjectedType -> 0L + Float::class.starProjectedType -> 0f + Boolean::class.starProjectedType -> false + else -> false + } + } + + + return constructor.call(*args.toTypedArray()) + } } diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/penalties/recallsuspension/RecallSuspensionDataSourceImpl.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/penalties/recallsuspension/RecallSuspensionDataSourceImpl.kt index f356a4bb2..9d08d7f07 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/penalties/recallsuspension/RecallSuspensionDataSourceImpl.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/penalties/recallsuspension/RecallSuspensionDataSourceImpl.kt @@ -9,7 +9,6 @@ import com.android.mediproject.core.network.onResponse import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf import javax.inject.Inject class RecallSuspensionDataSourceImpl @Inject constructor( @@ -30,7 +29,7 @@ class RecallSuspensionDataSourceImpl @Inject constructor( }, onFailure = { Result.failure(it) }).also { - flowOf(it) + emit(it) } } diff --git a/feature/news/src/main/java/com/android/mediproject/feature/news/NewsFragment.kt b/feature/news/src/main/java/com/android/mediproject/feature/news/NewsFragment.kt index c09ed1427..d68259b80 100644 --- a/feature/news/src/main/java/com/android/mediproject/feature/news/NewsFragment.kt +++ b/feature/news/src/main/java/com/android/mediproject/feature/news/NewsFragment.kt @@ -3,25 +3,18 @@ package com.android.mediproject.feature.news import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels -import androidx.navigation.fragment.navArgs +import com.android.mediproject.core.common.util.navArgs import com.android.mediproject.core.model.local.navargs.RecallDisposalArgs import com.android.mediproject.core.ui.base.BaseFragment import com.android.mediproject.feature.news.databinding.FragmentNewsBinding import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class NewsFragment : - BaseFragment(FragmentNewsBinding::inflate) { +class NewsFragment : BaseFragment(FragmentNewsBinding::inflate) { override val fragmentViewModel: NewsViewModel by viewModels() - private val recallDisposalArgs: RecallDisposalArgs by lazy { - try { - navArgs().value - } catch (e: Exception) { - RecallDisposalArgs("") - } - } + private val recallDisposalArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -31,5 +24,7 @@ class NewsFragment : NewsNavHost(arguments = recallDisposalArgs) } } + } + } \ No newline at end of file diff --git a/feature/news/src/main/java/com/android/mediproject/feature/news/NewsScreen.kt b/feature/news/src/main/java/com/android/mediproject/feature/news/NewsScreen.kt index 6098b3c45..f6ba7084f 100644 --- a/feature/news/src/main/java/com/android/mediproject/feature/news/NewsScreen.kt +++ b/feature/news/src/main/java/com/android/mediproject/feature/news/NewsScreen.kt @@ -58,7 +58,7 @@ fun NewsNavHost( "" } - val start = if (arguments.product.isNotEmpty()) "detailRecallSuspension" + val start = if (arguments.product.isNotEmpty()) "detailRecallSuspension/{product}" else "news" NavHost(navController = navController, startDestination = start) { @@ -68,8 +68,10 @@ fun NewsNavHost( } } composable( - "detailRecallSuspension/{product}", - arguments = listOf(navArgument("product") { type = NavType.StringType }) + "detailRecallSuspension/{product}", arguments = listOf(navArgument("product") { + type = NavType.StringType + defaultValue = arguments.product + }) ) { DetailRecallDisposalScreen() } diff --git a/feature/penalties/src/main/java/com/android/mediproject/feature/penalties/recentpenaltylist/RecentPenaltyListFragment.kt b/feature/penalties/src/main/java/com/android/mediproject/feature/penalties/recentpenaltylist/RecentPenaltyListFragment.kt index eb34a289c..b18344fda 100644 --- a/feature/penalties/src/main/java/com/android/mediproject/feature/penalties/recentpenaltylist/RecentPenaltyListFragment.kt +++ b/feature/penalties/src/main/java/com/android/mediproject/feature/penalties/recentpenaltylist/RecentPenaltyListFragment.kt @@ -2,9 +2,9 @@ package com.android.mediproject.feature.penalties.recentpenaltylist import android.os.Bundle import android.view.View -import androidx.core.net.toUri import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.android.mediproject.core.common.util.deepNavigate import com.android.mediproject.core.common.util.navigateByDeepLink import com.android.mediproject.core.common.viewmodel.UiState import com.android.mediproject.core.model.local.navargs.RecallDisposalArgs @@ -57,8 +57,8 @@ class RecentPenaltyListFragment : uiState.data.forEach { itemDto -> itemDto.onClick = { - findNavController().navigateByDeepLink( - "medilens://main/news_nav", RecallDisposalArgs(it.product) + findNavController().deepNavigate( + "medilens://main/news_nav", RecallDisposalArgs(it.product), ) } } @@ -82,7 +82,9 @@ class RecentPenaltyListFragment : binding.headerView.setOnExpandClickListener {} binding.headerView.setOnMoreClickListener { - findNavController().navigate("medilens://main/news_nav".toUri()) + findNavController().navigateByDeepLink( + "medilens://main/news_nav", RecallDisposalArgs(""), + ) } } } \ No newline at end of file