Skip to content

Commit 55b07dd

Browse files
update Tutorial6_1ConditionalNavigation
1 parent 2286975 commit 55b07dd

3 files changed

Lines changed: 165 additions & 61 deletions

File tree

Tutorial3-1Navigation/src/main/java/com/smarttoolfactory/tutorial3_1navigation/Tutorial4_1Deeplink.kt

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
package com.smarttoolfactory.tutorial3_1navigation
22

33
import android.Manifest
4-
import android.app.Notification
5-
import android.app.NotificationChannel
6-
import android.app.NotificationManager
7-
import android.app.PendingIntent
8-
import android.app.TaskStackBuilder
94
import android.content.Context
10-
import android.content.Intent
115
import android.content.pm.PackageManager
126
import android.net.Uri
137
import android.os.Build
@@ -49,10 +43,7 @@ import androidx.compose.ui.text.font.FontWeight
4943
import androidx.compose.ui.tooling.preview.Preview
5044
import androidx.compose.ui.unit.dp
5145
import androidx.compose.ui.unit.sp
52-
import androidx.core.app.NotificationCompat
5346
import androidx.core.content.ContextCompat
54-
import androidx.core.content.getSystemService
55-
import androidx.core.net.toUri
5647
import androidx.navigation.NavBackStackEntry
5748
import androidx.navigation.NavGraphBuilder
5849
import androidx.navigation.NavHostController
@@ -185,52 +176,10 @@ private fun SplashScreen(
185176
}
186177
}
187178

188-
private fun showNotification(context: Context) {
189-
val id = "exampleId"
190-
191-
val deepLinkIntent = Intent(
192-
Intent.ACTION_VIEW,
193-
"$uri/profile/$id".toUri(),
194-
context,
195-
MainActivity::class.java
196-
)
197-
198-
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder
199-
.create(context).run {
200-
addNextIntentWithParentStack(deepLinkIntent)
201-
getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE)
202-
}
203-
204-
205-
val notification: Notification = NotificationCompat.Builder(
206-
context,
207-
"channelId"
208-
)
209-
.setSmallIcon(R.drawable.ic_launcher_foreground)
210-
.setContentTitle("Go to app")
211-
.setContentText("Click to open Profile")
212-
.setPriority(NotificationCompat.PRIORITY_MAX)
213-
.setAutoCancel(true)
214-
.setContentIntent(deepLinkPendingIntent)
215-
.build()
216-
217-
val notificationManager = context.getSystemService() as NotificationManager?
218-
219-
notificationManager?.run {
220-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
221-
this.createNotificationChannel(
222-
NotificationChannel("channelId", "name", NotificationManager.IMPORTANCE_DEFAULT)
223-
)
224-
}
225-
this.notify(1, notification)
226-
}
227-
228-
}
229-
230179
@Composable
231180
private fun HomeScreen(
232181
onClick: (Profile) -> Unit,
233-
onOpenDeeplink: (Profile) -> Unit,
182+
onShowDeeplinkNotification: (Profile) -> Unit,
234183
) {
235184
Column(
236185
modifier = Modifier.fillMaxSize()
@@ -268,7 +217,7 @@ private fun HomeScreen(
268217

269218
IconButton(
270219
onClick = {
271-
onOpenDeeplink(it)
220+
onShowDeeplinkNotification(it)
272221
}
273222
) {
274223
Icon(

Tutorial3-1Navigation/src/main/java/com/smarttoolfactory/tutorial3_1navigation/Tutorial6_1ConditionalNavigation.kt

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
package com.smarttoolfactory.tutorial3_1navigation
44

5+
import android.Manifest
56
import android.annotation.SuppressLint
7+
import android.content.pm.PackageManager
8+
import android.net.Uri
9+
import android.os.Build
10+
import androidx.activity.compose.rememberLauncherForActivityResult
11+
import androidx.activity.result.contract.ActivityResultContracts
612
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection
713
import androidx.compose.animation.core.tween
814
import androidx.compose.foundation.background
915
import androidx.compose.foundation.clickable
1016
import androidx.compose.foundation.layout.Arrangement
1117
import androidx.compose.foundation.layout.Column
1218
import androidx.compose.foundation.layout.PaddingValues
19+
import androidx.compose.foundation.layout.Row
1320
import androidx.compose.foundation.layout.Spacer
1421
import androidx.compose.foundation.layout.fillMaxSize
1522
import androidx.compose.foundation.layout.fillMaxWidth
@@ -18,9 +25,13 @@ import androidx.compose.foundation.layout.padding
1825
import androidx.compose.foundation.lazy.LazyColumn
1926
import androidx.compose.foundation.lazy.items
2027
import androidx.compose.foundation.shape.RoundedCornerShape
28+
import androidx.compose.material.icons.Icons
29+
import androidx.compose.material.icons.filled.Notifications
2130
import androidx.compose.material3.Button
2231
import androidx.compose.material3.ExperimentalMaterial3Api
2332
import androidx.compose.material3.Icon
33+
import androidx.compose.material3.IconButton
34+
import androidx.compose.material3.MaterialTheme
2435
import androidx.compose.material3.NavigationBar
2536
import androidx.compose.material3.NavigationBarItem
2637
import androidx.compose.material3.OutlinedTextField
@@ -39,10 +50,12 @@ import androidx.compose.ui.Alignment
3950
import androidx.compose.ui.Modifier
4051
import androidx.compose.ui.draw.shadow
4152
import androidx.compose.ui.graphics.Color
53+
import androidx.compose.ui.platform.LocalContext
4254
import androidx.compose.ui.text.font.FontWeight
4355
import androidx.compose.ui.tooling.preview.Preview
4456
import androidx.compose.ui.unit.dp
4557
import androidx.compose.ui.unit.sp
58+
import androidx.core.content.ContextCompat
4659
import androidx.hilt.navigation.compose.hiltViewModel
4760
import androidx.lifecycle.ViewModel
4861
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -57,6 +70,7 @@ import androidx.navigation.compose.composable
5770
import androidx.navigation.compose.currentBackStackEntryAsState
5871
import androidx.navigation.compose.navigation
5972
import androidx.navigation.compose.rememberNavController
73+
import androidx.navigation.navDeepLink
6074
import androidx.navigation.toRoute
6175
import dagger.hilt.android.lifecycle.HiltViewModel
6276
import kotlinx.coroutines.delay
@@ -73,12 +87,52 @@ import kotlin.random.Random
7387
@Preview
7488
@Composable
7589
fun Tutorial6_1Screen() {
90+
91+
// In this example RegisterViewModel is shared by
92+
// RegisterGraph and BottomNavigationRoute.DashboardRoute
93+
// with HomeGraph NavBackEntry
94+
// When user is not logged in RegisterScreen is shown by navigating from
95+
// BottomNavigationRoute.DashboardRoute
96+
97+
// Also if user enters app via deeplink from terminal
98+
// or Notification startDestination is changed from Splash to ProfileGraph
99+
// to have conditional startDestination too.
100+
101+
76102
val navController = rememberNavController()
77103

104+
val context = LocalContext.current
105+
106+
var hasNotificationPermission by remember {
107+
mutableStateOf(
108+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
109+
110+
ContextCompat.checkSelfPermission(
111+
context,
112+
Manifest.permission.POST_NOTIFICATIONS
113+
) == PackageManager.PERMISSION_GRANTED
114+
} else true
115+
)
116+
}
117+
118+
val permissionRequest =
119+
rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) { result ->
120+
hasNotificationPermission = result
121+
122+
if (hasNotificationPermission) {
123+
showNotification(context)
124+
}
125+
}
126+
127+
val deeplink: Uri? = (LocalContext.current as? MainActivity)?.intent?.data
128+
val isDeeplink = deeplink != null
129+
130+
println("Deeplink: $deeplink")
131+
78132
NavHost(
79133
modifier = Modifier.fillMaxSize(),
80134
navController = navController,
81-
startDestination = Splash,
135+
startDestination = if (isDeeplink) ProfileGraph else Splash,
82136
enterTransition = {
83137
slideIntoContainer(
84138
towards = SlideDirection.Start,
@@ -120,6 +174,8 @@ fun Tutorial6_1Screen() {
120174
) {
121175

122176
navigation<RegisterGraph>(
177+
// 🔥🔥 Start destination should be class
178+
// or throws error for Serialized for Companion
123179
startDestination = SessionModel("", "")
124180
) {
125181
composable<SessionModel> { navBackStackEntry: NavBackStackEntry ->
@@ -161,16 +217,32 @@ fun Tutorial6_1Screen() {
161217

162218
// If registered open Home Screen
163219
if (registerViewModel.loggedIn) {
164-
MainContainer { route: Any, navBackStackEntry: NavBackStackEntry ->
220+
MainContainer(
221+
onShowDeeplinkNotification = {
222+
if (hasNotificationPermission) {
223+
showNotification(context)
224+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
225+
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
226+
}
227+
}
228+
) { route: Any, navBackStackEntry: NavBackStackEntry ->
165229
// Navigate only when life cycle is resumed for current screen
166230
if (navBackStackEntry.lifecycleIsResumed()) {
167231
navController.navigate(route = route)
168232
}
169233
}
170234
}
171235
}
236+
}
172237

173-
composable<Profile> { navBackStackEntry: NavBackStackEntry ->
238+
navigation<ProfileGraph>(
239+
startDestination = Profile("")
240+
) {
241+
composable<Profile>(
242+
deepLinks = listOf(
243+
navDeepLink<Profile>(basePath = "$uri/profile")
244+
)
245+
) { navBackStackEntry: NavBackStackEntry ->
174246
val profile: Profile = navBackStackEntry.toRoute<Profile>()
175247
Screen(profile.toString(), navController)
176248
}
@@ -181,6 +253,7 @@ fun Tutorial6_1Screen() {
181253
@SuppressLint("RestrictedApi")
182254
@Composable
183255
private fun MainContainer(
256+
onShowDeeplinkNotification: () -> Unit,
184257
onGoToProfileScreen: (
185258
route: Any,
186259
navBackStackEntry: NavBackStackEntry,
@@ -254,6 +327,7 @@ private fun MainContainer(
254327
) {
255328
addBottomNavigationGraph(
256329
nestedNavController = nestedNavController,
330+
onShowDeeplinkNotification = onShowDeeplinkNotification,
257331
onGoToProfileScreen = { route, navBackStackEntry ->
258332
onGoToProfileScreen(route, navBackStackEntry)
259333
},
@@ -271,6 +345,7 @@ private fun MainContainer(
271345
*/
272346
private fun NavGraphBuilder.addBottomNavigationGraph(
273347
nestedNavController: NavHostController,
348+
onShowDeeplinkNotification: () -> Unit,
274349
onGoToProfileScreen: (route: Any, navBackStackEntry: NavBackStackEntry) -> Unit,
275350
onBottomScreenClick: (route: Any, navBackStackEntry: NavBackStackEntry) -> Unit,
276351
) {
@@ -339,6 +414,7 @@ private fun NavGraphBuilder.addBottomNavigationGraph(
339414
composable<BottomNavigationRoute.FavoritesRoute> { from: NavBackStackEntry ->
340415
UsersScreen(
341416
homeViewModel = hiltViewModel(),
417+
onShowDeeplinkNotification = onShowDeeplinkNotification,
342418
onProfileClick = { userProfile ->
343419
onGoToProfileScreen(
344420
Profile("Name: ${userProfile.name}"),
@@ -386,6 +462,7 @@ private fun SplashScreen(
386462
private fun UsersScreen(
387463
homeViewModel: UsersVieModel,
388464
onProfileClick: (UserProfile) -> Unit,
465+
onShowDeeplinkNotification: () -> Unit,
389466
) {
390467

391468
val userList by homeViewModel.profileFlow.collectAsStateWithLifecycle()
@@ -397,17 +474,40 @@ private fun UsersScreen(
397474
) {
398475

399476
items(items = userList) {
400-
Text(
477+
478+
Row(
401479
modifier = Modifier
402480
.fillMaxWidth()
403-
.shadow(2.dp, RoundedCornerShape(16.dp))
481+
.shadow(4.dp, RoundedCornerShape(8.dp))
404482
.background(Color.White)
483+
.fillMaxWidth()
484+
.padding(start = 16.dp)
405485
.clickable {
406486
onProfileClick(it)
487+
},
488+
verticalAlignment = Alignment.CenterVertically
489+
) {
490+
Text(
491+
text = "Profile ${it.id}",
492+
fontSize = 18.sp
493+
)
494+
495+
Spacer(modifier = Modifier.weight(1f))
496+
497+
IconButton(
498+
onClick = {
499+
onShowDeeplinkNotification()
407500
}
408-
.padding(16.dp),
409-
text = "name: ${it.name}, id: ${it.id}"
410-
)
501+
) {
502+
Icon(
503+
tint = MaterialTheme.colorScheme.tertiary,
504+
imageVector = Icons.Default.Notifications,
505+
contentDescription = null
506+
)
507+
}
508+
}
509+
510+
411511
}
412512
}
413513
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.smarttoolfactory.tutorial3_1navigation
2+
3+
import android.app.Notification
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.app.PendingIntent
7+
import android.app.TaskStackBuilder
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.os.Build
11+
import androidx.core.app.NotificationCompat
12+
import androidx.core.content.getSystemService
13+
import androidx.core.net.toUri
14+
15+
fun showNotification(context: Context) {
16+
val id = "exampleId"
17+
18+
val deepLinkIntent = Intent(
19+
Intent.ACTION_VIEW,
20+
"$uri/profile/$id".toUri(),
21+
context,
22+
MainActivity::class.java
23+
)
24+
25+
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder
26+
.create(context).run {
27+
addNextIntentWithParentStack(deepLinkIntent)
28+
getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE)
29+
}
30+
31+
32+
val notification: Notification = NotificationCompat.Builder(
33+
context,
34+
"channelId"
35+
)
36+
.setSmallIcon(R.drawable.ic_launcher_foreground)
37+
.setContentTitle("Go to app")
38+
.setContentText("Click to open Profile")
39+
.setPriority(NotificationCompat.PRIORITY_MAX)
40+
.setAutoCancel(true)
41+
.setContentIntent(deepLinkPendingIntent)
42+
.build()
43+
44+
val notificationManager = context.getSystemService() as NotificationManager?
45+
46+
notificationManager?.run {
47+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
48+
this.createNotificationChannel(
49+
NotificationChannel("channelId", "name", NotificationManager.IMPORTANCE_DEFAULT)
50+
)
51+
}
52+
this.notify(1, notification)
53+
}
54+
55+
}

0 commit comments

Comments
 (0)