개인 프로젝트 진행 중 Notification 클릭 시 특정화면으로 이동해야 하는 기능을 구현 중
앱이 실행중인 상태라면 기존 화면을 유지한 채로 이동해야하는 화면을 띄우는 작업에서 어떻게 처리했고 여러 시도를 해보면서 얻은 지식을 기록하고자 한다.
우선 Compose에서 특정 화면으로 이동해야하는 기능을 구현하기 위해 DeepLink를 사용하기로 하였다.
Compose Navigation을 사용 중이었기에 DeepLink에 대한 내용을 선언하기만 하면 자동으로 처리해주는 부분이 편리하기 때문이다.
우선 NavHost에 딥링크 선언이 필요하다.
kotlinx serialization을 이용해 Route를 선언하고 활용했다.
@Composable
fun DeepLinkSampleApp(
scaffoldPadding: PaddingValues,
) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Route.Home,
modifier = Modifier
.fillMaxSize()
.padding(scaffoldPadding),
) {
composable<Route.Home> {
HomeScreen {
navController.navigate(Route.DeepLink(number = 1))
}
}
composable<Route.DeepLink>(
//딥링크 선언
deepLinks = listOf(
navDeepLink {
uriPattern = "deeplink://sample?number={number}"
}
)
) { navBackStackEntry ->
val number = navBackStackEntry.toRoute<Route.DeepLink>().number
DeepLinkScreen(
number = number
)
}
}
}
또 Manifest에 Intent Filter를 선언해주어야 딥링크가 올바르게 작동한다.

여기까지만 해도 딥링크는 처리가 된다.
하지만 Acitivty의 onCreate 에 로그를 찍어보면 딥링크 실행마다 onCreate가 다시 실행된다.
즉 화면이 다시 시작하기 때문에 Compose UI 또한 다시 그려지면서 해당화면으로 이동하지만 그간의 기록이 초기화된다.
내 프로젝트에서는 Container Screen이 존재한 상태에서 하단 탭으로 화면을 이동할 수 있어 유저가 이동한 화면의 기록이 홈화면으로 초기화 된다.
(예를 들어 설정 탭으로 이동해놓고 딥링크를 사용하면 딥링크를 적용한 화면은 실행되지만 뒤로갔을 때 메인화면이 모두 초기화 되있다.)
나의 경우 탭당 단 한 화면만 볼 수 있었지만 만약 탭 화면내에서 여러 화면으로 이동이 가능한 앱이라고 했을 때 딥링크를 사용하면 유저가 이동했던 화면이 아니라 초기화면부터 재시작하기 때문에 유저가 다시 해당화면으로 진입하고 싶다면 불편함을 느낄 수 밖에 없다.
따라서 Activty가 재시작하지 않도록 하기 위해 singleTop을 적용하기로 했다.
Manifest Activity에 launchMode를 singleTop으로 설정한다음 딥링크 테스트를 해보면 제대로 작동 하지 않는 것을 볼 수 있다.
공식문서에서 확인할 수 있는 데 launchMode를 standard가 아닌 경우에는 수동으로 딥링크를 핸들링해야한다고 적혀있다.
액티비티를 재사용하는 경우 onNewIntent를 통해 딥링크를 확인하고 처리할 수 있다.
하지만 Activity에서 처리해야되기 때문에 NavHostController를 Activity쪽에서 알고 있어야한다.
그렇게 되면 컨트롤러를 호이스팅해야하고 코드가 복잡해진다.
따라서 Activity에 onNewIntent 리스너를 등록하여 해결했다.
//CompositionLocalProvider 사용
val LocalActivity = staticCompositionLocalOf<ComponentActivity> { error("no activity") }
//MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("DeepLinkSample", "Activity onCreate")
enableEdgeToEdge()
setContent {
DeepLinkSampleTheme {
CompositionLocalProvider(
LocalActivity provides this
) {
Scaffold { innerPadding ->
DeepLinkSampleApp(
scaffoldPadding = innerPadding
)
}
}
}
}
}
}
//DeepLinkApp
@Composable
fun DeepLinkSampleApp(
scaffoldPadding: PaddingValues,
) {
val navController = rememberNavController()
val activity = LocalActivity.current
DisposableEffect(Unit) {
val onNewIntentConsumer = Consumer<Intent> {
val number = it.data?.getQueryParameter("number")?.toInt() ?: 0
navController.navigate(
Route.DeepLink(
number = number
)
)
}
activity.addOnNewIntentListener(onNewIntentConsumer)
onDispose {
activity.removeOnNewIntentListener(onNewIntentConsumer)
}
}
//...
}
나의 경우 Activity 객체를 CompositalLocal을 이용하여 제공하고 DisposableEffect를 통해 onNewIntent 리스너를 등록/해제하였다.
리스너에서 NavHostController.handleDeepLink() 메소드를 처음에 적용했었는데 해당 메소드 또한 Acitivity를 재시작하는 문제가 있어 직접 navigation 해주는 방식으로 변경하였다.
직접 Navigation한 경우handleDeepLink() 코드를 보면 기존 작업스택을 확인할 수 없어 전체 스택을 다시 시작한다는 내용의 주석이 보인다.
그 아래 코드를 살펴보면 기존 Activity를 종료하고 새로운 TaskStackBuilder를 시작한다.
rememberSaveable을 사용해야만 count가 유지된다.
rememberNavController() 구현 부분을 보면 rememberSaveable로 NavHostController를 제공해 navigation후에도 백스택 유지가 되는것으로 보인다.