지금 진행하는 프로젝트는 Jetpack Compose 기반으로 되어있는데, Service에서 업로드 작업 후 노티피케이션을 누르면 해당 화면으로 이동하게 만들고 싶었다.
그런데... 내 화면은 Compose로 되어있기 때문에 해당 액티비티로 이동할수가 없었다.
이 프로젝트는 메인 액티비티 하나를 쓰는 싱글 액티비티에 가까운(로그인용 액티비티까지 두개지만 이건 한번 로그인을 한 이후엔 볼 일이 거의 없는 화면이므로) 구조여서 메인액티비티에서 해결을 봐야 했다.
아니면 이런 상황이 자주 있다면, 아예 라우팅용 액티비티를 만들어서 거기서 라우팅 처리를 다 한다면 깔끔하겠지만... 굳이?
지금 상황에서는 디벨롭 할 기능들을 생각해도 액티비티에서 인텐트를 받아 라우팅 시켜줄 일이 없을 것 같아서 일단 메인액티비티에서 구현해주기로 했다.
아니면 딥링크를 사용할 수도 있는데, 이거 하나를 위해 딥링크를 구현한다고? 관리해야하는 string값이 늘어나는게 좀 부담이 되기도 한다.
정리하자면,
중에 1번을 선택했다는 것이다.
intent에 Activity로 이동하는 것 뿐만 아니라, 우리가 어디로 이동할거라는 Extra의 key와 value를 담아줘야한다.
key야 string으로 자유롭게 지정해주면 되지만, value로 무엇을 넘길것인가에 대한 생각은 좀 해봐야 한다.
const val NAVIGATE_TO_SCREEN_KEY = "navigate_to_screen"
key는 const로 만들어놓고 재사용하도록 했다. 휴먼에러 멈춰!
나는 compose navigation에서도, serializable을 사용한 방법으로 네비게이션을 설계했기 때문에 route name을 사용하지 않는다.
다행히 extra는 Parcelable을 value에 넣을수 있도록 지원하고 있기 때문에, 설계한 그대로 목적지에 @Parcelize 어노테이션을 붙이고, Parcelable을 상속받아 사용했다.
@Parcelize // 어노테이션과
@Serializable
data object RecipeGraph: BottomTab, Parcelable { // Parcelable 구현은 이 때 붙였습니다!
...
}
extra에서 요구하는 Serializable과 Navigation에서 사용한 Serializable은 다르기 때문!

⬆️요게 extra에 들어가는 Serializable(Java)

⬆️ 요게 네비게이션 설계할때 사용한 Serializable(Kotlin)
// 클릭 시 메인 액티비티로 이동하는 PendingIntent
val pendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java).apply {
putExtra(NAVIGATE_TO_SCREEN_KEY, Route.RecipeGraph)
},
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
extra의 value값으로 당당히 Route 객체를 넣어줄수 있게 되었다!
notificationManager.notify(
notificationId,
createBasicNotificationBuilder(
context = context,
title = context.getString(R.string.recipe_upload_service_complete_notification_title),
text = context.getString(R.string.recipe_upload_service_complete_notification_content)
)
.setContentIntent(pendingIntent) // ✨
.build()
)
이제 노티피케이션의 setContentIntent에 만들어준 pendingIntent를 넣어주면 된다.
현재 프로젝트의 구조는 메인 액티비티에서 바로 NavHost를 호출하지 않고, Entry Point를 두어 최상위 컴포저블로 사용하고 있다.
앱 바, 바텀 네비게이션 등 네비게이션 처리에 필요한 공통 컴포넌트들로 Scaffold를 사용해 만들어져있고, navController도 여기서 관리하고 있다. 그리고 그 하위에 NavHost가 자리하고 있다.
이 얘길 왜 하냐면, NavHost까지 목적지를 내려줘야 하기 때문이다...!
EntryPoint 하위에 NavHost가 있어서, EntryPoint 진입 시점에는 아직 앱에서 네비게이션 설계가 안 된 상태이기 때문에 네비게이팅이 안 된다!
그래서 최소 NavHost, 아니면 그 밑에서 네비게이팅 처리를 해 줘야 정상 동작한다.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setContent {
val destination =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) intent.getParcelableExtra(
NAVIGATE_TO_SCREEN_KEY,
Route.BottomTab::class.java
) else intent.getParcelableExtra(NAVIGATE_TO_SCREEN_KEY)
EntryPointScreen(destination ?: Route.RecipeGraph)
}
}
우리의 친절한 라이프사이클 메서드는 인텐트를 받았을때 동작하는 메서드도 포함되어있다.
onNewIntent에서 getParcelableExtra를 통해 객체를 받아 줄 것이다.
(Route.BottomTab은 그래프를 정의한 네비게이션 설계 객체입니다!)

else문에 있는 getParcelableExtra가 deprecated되고, if문에 있는 형태로 바뀌었는데 이건 또 버전 33 이상을 요구한다.
버전 어노테이션 붙여주든지, 나처럼 if-else로 처리하든지는 선택하면 될 듯 하다.
@Composable
fun EntryPointScreen(
destination: Route.BottomTab = Route.ScheduleGraph
)
사실 거쳐가기만 합니다. 대신 MainActivity의 onCreate 시점에는 destination이 없으므로 메인 화면으로 기본값을 주었습니다!
CustomNavHost(
paddingValues = paddingValues,
commonState = commonState,
navController = navController,
destination = destination // ✨
)
@Composable
fun CustomNavHost(
paddingValues: PaddingValues,
commonState: CommonState,
navController: NavHostController,
destination: Route.BottomTab
) {
LaunchedEffect(Unit) {
navController.navigate(destination)
}
NavHost(
navController = navController,
startDestination = Route.ScheduleGraph,
modifier = Modifier.padding(paddingValues = paddingValues)
) { ... }
드디어 NavHost!
Launch 시점에 네비게이팅 해줄거니까 LaunchedEffect 사용해서 처리해줍니다.
귀찮은 과정 없이 받은 그대로 navigate 해줄 수 있습니다!
이제 routeName이나 DeepLink 없이 깔끔하게 Pending Intent 처리가 되었습니다!👏🏻👏🏻👏🏻👏🏻👏🏻