Skip to content

Commit a506e60

Browse files
authored
Merge pull request #114 from android/onenavhost
[Cherry Pick] Use one NavHost (with a single navigation scaffold) (#102)
2 parents 5263dff + 77b80fc commit a506e60

File tree

17 files changed

+486
-442
lines changed

17 files changed

+486
-442
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
plugins {
1919
alias(libs.plugins.androidApplication)
2020
alias(libs.plugins.baselineprofile)
21+
alias(libs.plugins.compose.compiler)
2122
alias(libs.plugins.hilt)
2223
alias(libs.plugins.kotlinAndroid)
24+
alias(libs.plugins.kotlinSerialization)
2325
alias(libs.plugins.ksp)
2426
alias(libs.plugins.secrets)
2527
}
@@ -107,6 +109,7 @@ dependencies {
107109

108110
implementation(libs.activity.compose)
109111
implementation(libs.navigation.compose)
112+
implementation(libs.kotlinx.serialization.json)
110113

111114
implementation(libs.accompanist.painter)
112115
implementation(libs.accompanist.permissions)

app/src/androidTest/java/com/google/android/samples/socialite/ui/home/HomeViewModelTest.kt renamed to app/src/androidTest/java/com/google/android/samples/socialite/ui/home/ChatListViewModelTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
2020
import app.cash.turbine.test
2121
import com.google.android.samples.socialite.awaitNotEmpty
2222
import com.google.android.samples.socialite.repository.createTestRepository
23+
import com.google.android.samples.socialite.ui.home.chatlist.ChatListViewModel
2324
import com.google.common.truth.Truth.assertThat
2425
import kotlinx.coroutines.test.runTest
2526
import org.junit.Test
2627
import org.junit.runner.RunWith
2728

2829
@RunWith(AndroidJUnit4::class)
29-
class HomeViewModelTest {
30+
class ChatListViewModelTest {
3031

3132
@Test
3233
fun initialize() = runTest {
33-
val viewModel = HomeViewModel(createTestRepository())
34+
val viewModel = ChatListViewModel(createTestRepository())
3435
viewModel.chats.test {
3536
assertThat(awaitNotEmpty()).hasSize(4)
3637
}

app/src/main/java/com/google/android/samples/socialite/ui/Main.kt

Lines changed: 100 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,33 @@ import android.app.Activity
2020
import android.content.Intent
2121
import android.content.pm.ActivityInfo
2222
import android.os.Bundle
23-
import androidx.compose.animation.EnterTransition
2423
import androidx.compose.animation.core.FastOutLinearInEasing
2524
import androidx.compose.animation.core.FastOutSlowInEasing
2625
import androidx.compose.animation.core.tween
2726
import androidx.compose.animation.fadeIn
2827
import androidx.compose.animation.fadeOut
29-
import androidx.compose.animation.scaleOut
3028
import androidx.compose.foundation.layout.fillMaxSize
3129
import androidx.compose.runtime.Composable
3230
import androidx.compose.ui.Modifier
33-
import androidx.compose.ui.graphics.TransformOrigin
3431
import androidx.compose.ui.platform.LocalContext
3532
import androidx.navigation.NavController
3633
import androidx.navigation.NavDestination
37-
import androidx.navigation.NavType
34+
import androidx.navigation.NavDestination.Companion.hasRoute
3835
import androidx.navigation.compose.NavHost
3936
import androidx.navigation.compose.composable
4037
import androidx.navigation.compose.rememberNavController
41-
import androidx.navigation.navArgument
4238
import androidx.navigation.navDeepLink
39+
import androidx.navigation.toRoute
4340
import com.google.android.samples.socialite.model.extractChatId
4441
import com.google.android.samples.socialite.ui.camera.Camera
4542
import com.google.android.samples.socialite.ui.camera.Media
4643
import com.google.android.samples.socialite.ui.camera.MediaType
4744
import com.google.android.samples.socialite.ui.chat.ChatScreen
48-
import com.google.android.samples.socialite.ui.home.Home
45+
import com.google.android.samples.socialite.ui.home.chatlist.ChatList
46+
import com.google.android.samples.socialite.ui.home.settings.Settings
47+
import com.google.android.samples.socialite.ui.home.timeline.Timeline
48+
import com.google.android.samples.socialite.ui.navigation.Route
49+
import com.google.android.samples.socialite.ui.navigation.SocialiteNavSuite
4950
import com.google.android.samples.socialite.ui.photopicker.navigation.navigateToPhotoPicker
5051
import com.google.android.samples.socialite.ui.photopicker.navigation.photoPickerScreen
5152
import com.google.android.samples.socialite.ui.player.VideoPlayerScreen
@@ -69,133 +70,123 @@ fun MainNavigation(
6970
val activity = LocalContext.current as Activity
7071
val navController = rememberNavController()
7172

72-
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, _: Bundle? ->
73+
navController.addOnDestinationChangedListener { _: NavController, destination: NavDestination, _: Bundle? ->
7374
// Lock the layout of the Camera screen to portrait so that the UI layout remains
7475
// constant, even on orientation changes. Note that the camera is still aware of
7576
// orientation, and will assign the correct edge as the bottom of the photo or video.
76-
if (navDestination.route?.contains("camera") == true) {
77+
if (destination.hasRoute<Route.Camera>()) {
7778
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
7879
} else {
7980
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
8081
}
8182
}
8283

83-
NavHost(
84+
SocialiteNavSuite(
8485
navController = navController,
85-
startDestination = "home",
86-
popExitTransition = {
87-
scaleOut(
88-
targetScale = 0.9f,
89-
transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.5f),
90-
)
91-
},
92-
popEnterTransition = {
93-
EnterTransition.None
94-
},
9586
modifier = modifier,
9687
) {
97-
composable(
98-
route = "home",
88+
NavHost(
89+
navController = navController,
90+
startDestination = Route.ChatsList,
9991
) {
100-
Home(
101-
modifier = Modifier.fillMaxSize(),
102-
onChatClicked = { chatId -> navController.navigate("chat/$chatId") },
103-
)
104-
}
105-
composable(
106-
route = "chat/{chatId}?text={text}",
107-
arguments = listOf(
108-
navArgument("chatId") { type = NavType.LongType },
109-
navArgument("text") { defaultValue = "" },
110-
),
111-
deepLinks = listOf(
112-
navDeepLink {
113-
action = Intent.ACTION_VIEW
114-
uriPattern = "https://socialite.google.com/chat/{chatId}"
115-
},
116-
),
117-
) { backStackEntry ->
118-
val chatId = backStackEntry.arguments?.getLong("chatId") ?: 0L
119-
val text = backStackEntry.arguments?.getString("text")
120-
ChatScreen(
121-
chatId = chatId,
122-
foreground = true,
123-
onBackPressed = { navController.popBackStack() },
124-
onCameraClick = { navController.navigate("chat/$chatId/camera") },
125-
onPhotoPickerClick = { navController.navigateToPhotoPicker(chatId) },
126-
onVideoClick = { uri -> navController.navigate("videoPlayer?uri=$uri") },
127-
prefilledText = text,
128-
modifier = Modifier.fillMaxSize(),
129-
)
130-
}
131-
composable(
132-
route = "chat/{chatId}/camera",
133-
arguments = listOf(
134-
navArgument("chatId") { type = NavType.LongType },
135-
),
136-
) { backStackEntry ->
137-
val chatId = backStackEntry.arguments?.getLong("chatId") ?: 0L
138-
Camera(
139-
onMediaCaptured = { capturedMedia: Media? ->
140-
when (capturedMedia?.mediaType) {
141-
MediaType.PHOTO -> {
142-
navController.popBackStack()
143-
}
92+
composable<Route.ChatsList> {
93+
ChatList(
94+
onChatClicked = { chatId -> navController.navigate(Route.ChatThread(chatId)) },
95+
modifier = Modifier.fillMaxSize(),
96+
)
97+
}
14498

145-
MediaType.VIDEO -> {
146-
navController.navigate("videoEdit?uri=${capturedMedia.uri}&chatId=$chatId")
147-
}
99+
composable<Route.Timeline> {
100+
Timeline(Modifier.fillMaxSize())
101+
}
102+
103+
composable<Route.Settings> {
104+
Settings(Modifier.fillMaxSize())
105+
}
106+
107+
composable<Route.ChatThread>(
108+
deepLinks = listOf(
109+
navDeepLink {
110+
action = Intent.ACTION_VIEW
111+
uriPattern = "https://socialite.google.com/chat/{chatId}"
112+
},
113+
),
114+
) { backStackEntry ->
115+
val route: Route.ChatThread = backStackEntry.toRoute()
116+
val chatId = route.chatId
117+
ChatScreen(
118+
chatId = chatId,
119+
foreground = true,
120+
onBackPressed = { navController.popBackStack() },
121+
onCameraClick = { navController.navigate(Route.Camera(chatId)) },
122+
onPhotoPickerClick = { navController.navigateToPhotoPicker(chatId) },
123+
onVideoClick = { uri -> navController.navigate(Route.VideoPlayer(uri)) },
124+
prefilledText = route.text,
125+
modifier = Modifier.fillMaxSize(),
126+
)
127+
}
148128

149-
else -> {
150-
// No media to use.
151-
navController.popBackStack()
129+
composable<Route.Camera> { backStackEntry ->
130+
val route: Route.Camera = backStackEntry.toRoute()
131+
val chatId = route.chatId
132+
Camera(
133+
onMediaCaptured = { capturedMedia: Media? ->
134+
when (capturedMedia?.mediaType) {
135+
MediaType.PHOTO -> {
136+
navController.popBackStack()
137+
}
138+
139+
MediaType.VIDEO -> {
140+
navController.navigate(
141+
Route.VideoEdit(
142+
chatId,
143+
capturedMedia.uri.toString(),
144+
),
145+
)
146+
}
147+
148+
else -> {
149+
// No media to use.
150+
navController.popBackStack()
151+
}
152152
}
153-
}
154-
},
155-
chatId = chatId,
156-
modifier = Modifier.fillMaxSize(),
157-
)
158-
}
153+
},
154+
modifier = Modifier.fillMaxSize(),
155+
)
156+
}
159157

160-
// Invoke PhotoPicker to select photo or video from device gallery
161-
photoPickerScreen(
162-
onPhotoPicked = navController::popBackStack,
163-
)
164-
165-
composable(
166-
route = "videoEdit?uri={videoUri}&chatId={chatId}",
167-
arguments = listOf(
168-
navArgument("videoUri") { type = NavType.StringType },
169-
navArgument("chatId") { type = NavType.LongType },
170-
),
171-
) { backStackEntry ->
172-
val chatId = backStackEntry.arguments?.getLong("chatId") ?: 0L
173-
val videoUri = backStackEntry.arguments?.getString("videoUri") ?: ""
174-
VideoEditScreen(
175-
chatId = chatId,
176-
uri = videoUri,
177-
onCloseButtonClicked = { navController.popBackStack() },
178-
navController = navController,
179-
)
180-
}
181-
composable(
182-
route = "videoPlayer?uri={videoUri}",
183-
arguments = listOf(
184-
navArgument("videoUri") { type = NavType.StringType },
185-
),
186-
) { backStackEntry ->
187-
val videoUri = backStackEntry.arguments?.getString("videoUri") ?: ""
188-
VideoPlayerScreen(
189-
uri = videoUri,
190-
onCloseButtonClicked = { navController.popBackStack() },
158+
// Invoke PhotoPicker to select photo or video from device gallery
159+
photoPickerScreen(
160+
onPhotoPicked = navController::popBackStack,
191161
)
162+
163+
composable<Route.VideoEdit> { backStackEntry ->
164+
val route: Route.VideoEdit = backStackEntry.toRoute()
165+
val chatId = route.chatId
166+
val videoUri = route.uri
167+
VideoEditScreen(
168+
chatId = chatId,
169+
uri = videoUri,
170+
onCloseButtonClicked = { navController.popBackStack() },
171+
navController = navController,
172+
)
173+
}
174+
175+
composable<Route.VideoPlayer> { backStackEntry ->
176+
val route: Route.VideoPlayer = backStackEntry.toRoute()
177+
val videoUri = route.uri
178+
VideoPlayerScreen(
179+
uri = videoUri,
180+
onCloseButtonClicked = { navController.popBackStack() },
181+
)
182+
}
192183
}
193184
}
194185

195186
if (shortcutParams != null) {
196187
val chatId = extractChatId(shortcutParams.shortcutId)
197188
val text = shortcutParams.text
198-
navController.navigate("chat/$chatId?text=$text")
189+
navController.navigate(Route.ChatThread(chatId, text))
199190
}
200191
}
201192

app/src/main/java/com/google/android/samples/socialite/ui/camera/Camera.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ import kotlinx.coroutines.asExecutor
7171
@OptIn(ExperimentalPermissionsApi::class)
7272
@Composable
7373
fun Camera(
74-
chatId: Long,
7574
onMediaCaptured: (Media?) -> Unit,
7675
modifier: Modifier = Modifier,
7776
viewModel: CameraViewModel = hiltViewModel(),
@@ -86,8 +85,6 @@ fun Camera(
8685
),
8786
)
8887

89-
viewModel.setChatId(chatId)
90-
9188
val lifecycleOwner = LocalLifecycleOwner.current
9289
val context = LocalContext.current
9390

@@ -97,8 +94,8 @@ fun Camera(
9794
val windowInfoTracker = WindowInfoTracker.getOrCreate(context)
9895
windowInfoTracker.windowLayoutInfo(context).collect { newLayoutInfo ->
9996
try {
100-
val foldingFeature = newLayoutInfo?.displayFeatures
101-
?.firstOrNull { it is FoldingFeature } as FoldingFeature
97+
val foldingFeature = newLayoutInfo.displayFeatures
98+
.filterIsInstance<FoldingFeature>().firstOrNull()
10299
isLayoutUnfolded = (foldingFeature != null)
103100
} catch (e: Exception) {
104101
// If there was an issue detecting a foldable in the open position, default

0 commit comments

Comments
 (0)