Skip to content

Commit eea3828

Browse files
authored
Merge pull request #120 from android/krishna/nav3_resultScreenImp
Reland the Nav3 Migration of Results Screen
2 parents 43f81b9 + c7077be commit eea3828

File tree

21 files changed

+400
-257
lines changed

21 files changed

+400
-257
lines changed

app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,21 @@ import androidx.compose.runtime.remember
3232
import androidx.compose.runtime.setValue
3333
import androidx.compose.ui.platform.LocalContext
3434
import androidx.compose.ui.unit.IntOffset
35+
import androidx.hilt.navigation.compose.hiltViewModel
3536
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
3637
import androidx.navigation3.runtime.entry
3738
import androidx.navigation3.runtime.entryProvider
3839
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
3940
import androidx.navigation3.ui.NavDisplay
4041
import com.android.developers.androidify.camera.CameraPreviewScreen
4142
import com.android.developers.androidify.creation.CreationScreen
43+
import com.android.developers.androidify.creation.CreationViewModel
44+
import com.android.developers.androidify.customize.CustomizeAndExportScreen
45+
import com.android.developers.androidify.customize.CustomizeExportViewModel
4246
import com.android.developers.androidify.home.AboutScreen
4347
import com.android.developers.androidify.home.HomeScreen
48+
import com.android.developers.androidify.results.ResultsScreen
49+
import com.android.developers.androidify.results.ResultsViewModel
4450
import com.android.developers.androidify.theme.transitions.ColorSplashTransitionScreen
4551
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
4652

@@ -92,14 +98,20 @@ fun MainNavigation() {
9298
CameraPreviewScreen(
9399
onImageCaptured = { uri ->
94100
backStack.removeAll { it is Create }
95-
backStack.add(Create(uri.toString()))
101+
backStack.add(Create(uri))
96102
backStack.removeAll { it is Camera }
97103
},
98104
)
99105
}
100106
entry<Create> { createKey ->
107+
val creationViewModel = hiltViewModel<CreationViewModel, CreationViewModel.Factory>(
108+
creationCallback = { factory ->
109+
factory.create(
110+
originalImageUrl = createKey.fileName,
111+
)
112+
},
113+
)
101114
CreationScreen(
102-
createKey.fileName,
103115
onCameraPressed = {
104116
backStack.removeAll { it is Camera }
105117
backStack.add(Camera)
@@ -110,6 +122,64 @@ fun MainNavigation() {
110122
onAboutPressed = {
111123
backStack.add(About)
112124
},
125+
onImageCreated = { resultImageUri, prompt, originalImageUri ->
126+
backStack.removeAll { it is Result }
127+
backStack.add(
128+
Result(
129+
resultImageUri = resultImageUri,
130+
prompt = prompt,
131+
originalImageUri = originalImageUri,
132+
),
133+
)
134+
},
135+
creationViewModel = creationViewModel,
136+
)
137+
}
138+
entry<Result> { resultKey ->
139+
val resultsViewModel = hiltViewModel<ResultsViewModel, ResultsViewModel.Factory>(
140+
creationCallback = { factory ->
141+
factory.create(
142+
resultImageUrl = resultKey.resultImageUri,
143+
originalImageUrl = resultKey.originalImageUri,
144+
promptText = resultKey.prompt,
145+
)
146+
},
147+
)
148+
ResultsScreen(
149+
onNextPress = { resultImageUri, originalImageUri ->
150+
backStack.add(
151+
CustomizeExport(
152+
resultImageUri = resultImageUri,
153+
originalImageUri = originalImageUri,
154+
),
155+
)
156+
},
157+
onAboutPress = {
158+
backStack.add(About)
159+
},
160+
onBackPress = {
161+
backStack.removeLastOrNull()
162+
},
163+
viewModel = resultsViewModel,
164+
)
165+
}
166+
entry<CustomizeExport> { shareKey ->
167+
val customizeExportViewModel = hiltViewModel<CustomizeExportViewModel, CustomizeExportViewModel.Factory>(
168+
creationCallback = { factory ->
169+
factory.create(
170+
resultImageUrl = shareKey.resultImageUri,
171+
originalImageUrl = shareKey.originalImageUri,
172+
)
173+
},
174+
)
175+
CustomizeAndExportScreen(
176+
onBackPress = {
177+
backStack.removeLastOrNull()
178+
},
179+
onInfoPress = {
180+
backStack.add(About)
181+
},
182+
viewModel = customizeExportViewModel,
113183
)
114184
}
115185
entry<About> {

app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.android.developers.androidify.navigation
1919

20+
import android.net.Uri
2021
import kotlinx.serialization.ExperimentalSerializationApi
2122
import kotlinx.serialization.Serializable
2223

@@ -26,10 +27,39 @@ sealed interface NavigationRoute
2627
data object Home : NavigationRoute
2728

2829
@Serializable
29-
data class Create(val fileName: String? = null, val prompt: String? = null) : NavigationRoute
30+
data class Create(
31+
@Serializable(with = UriSerializer::class) val fileName: Uri? = null,
32+
val prompt: String? = null,
33+
) : NavigationRoute
3034

3135
@Serializable
3236
object Camera : NavigationRoute
3337

3438
@Serializable
3539
object About : NavigationRoute
40+
41+
/**
42+
* Represents the result of an image generation process, used for navigation.
43+
*
44+
* @param resultImageUri The URI of the generated image.
45+
* @param originalImageUri The URI of the original image used as a base for generation, if any.
46+
* @param prompt The text prompt used to generate the image, if any.
47+
*/
48+
@Serializable
49+
data class Result(
50+
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
51+
@Serializable(with = UriSerializer::class) val originalImageUri: Uri? = null,
52+
val prompt: String? = null,
53+
) : NavigationRoute
54+
55+
/**
56+
* Represents the navigation route to the screen for customizing and exporting a generated image.
57+
*
58+
* @param resultImageUri The URI of the generated image to be customized.
59+
* @param originalImageUri The URI of the original image, passed along for context.
60+
*/
61+
@Serializable
62+
data class CustomizeExport(
63+
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
64+
@Serializable(with = UriSerializer::class) val originalImageUri: Uri?,
65+
) : NavigationRoute
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.android.developers.androidify.navigation
17+
18+
import android.net.Uri
19+
import androidx.core.net.toUri
20+
import kotlinx.serialization.KSerializer
21+
import kotlinx.serialization.descriptors.PrimitiveKind
22+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
23+
import kotlinx.serialization.descriptors.SerialDescriptor
24+
import kotlinx.serialization.encoding.Decoder
25+
import kotlinx.serialization.encoding.Encoder
26+
27+
object UriSerializer : KSerializer<Uri> {
28+
override val descriptor: SerialDescriptor =
29+
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
30+
31+
override fun serialize(encoder: Encoder, value: Uri) {
32+
encoder.encodeString(value.toString())
33+
}
34+
35+
override fun deserialize(decoder: Decoder): Uri = decoder.decodeString().toUri()
36+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.android.developers.testing.data
17+
18+
import android.graphics.Bitmap
19+
20+
val bitmapSample = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)

core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,8 @@ class TestFileProvider : LocalFileProvider {
6464
): Uri {
6565
TODO("Not yet implemented")
6666
}
67+
68+
override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
69+
return bitmapSample
70+
}
6771
}

core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.android.developers.androidify.util
1818
import android.app.Application
1919
import android.content.ContentValues
2020
import android.graphics.Bitmap
21+
import android.graphics.BitmapFactory
2122
import android.net.Uri
2223
import android.os.Build
2324
import android.os.Environment
@@ -53,6 +54,9 @@ interface LocalFileProvider {
5354

5455
@WorkerThread
5556
suspend fun saveUriToSharedStorage(inputUri: Uri, fileName: String, mimeType: String): Uri
57+
58+
@WorkerThread
59+
suspend fun loadBitmapFromUri(uri: Uri): Bitmap?
5660
}
5761

5862
@Singleton
@@ -120,6 +124,20 @@ class LocalFileProviderImpl @Inject constructor(
120124
return@withContext newUri
121125
}
122126

127+
override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
128+
return withContext(ioDispatcher) {
129+
try {
130+
application.contentResolver.openInputStream(uri)?.use {
131+
return@withContext BitmapFactory.decodeStream(it)
132+
}
133+
null
134+
} catch (e: Exception) {
135+
e.printStackTrace()
136+
null
137+
}
138+
}
139+
}
140+
123141
@Throws(IOException::class)
124142
@WorkerThread
125143
private fun saveFileToUri(file: File, uri: Uri) {

feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt

Lines changed: 18 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,13 @@ import androidx.compose.ui.text.style.TextAlign
120120
import androidx.compose.ui.tooling.preview.Preview
121121
import androidx.compose.ui.unit.dp
122122
import androidx.compose.ui.unit.sp
123-
import androidx.core.net.toUri
124123
import androidx.graphics.shapes.RoundedPolygon
125124
import androidx.graphics.shapes.rectangle
126-
import androidx.hilt.navigation.compose.hiltViewModel
127125
import androidx.lifecycle.compose.collectAsStateWithLifecycle
128126
import coil3.compose.AsyncImage
129127
import coil3.request.ImageRequest
130128
import coil3.request.crossfade
131-
import com.android.developers.androidify.customize.CustomizeAndExportScreen
132-
import com.android.developers.androidify.customize.CustomizeExportViewModel
133129
import com.android.developers.androidify.data.DropBehaviourFactory
134-
import com.android.developers.androidify.results.ResultsScreen
135130
import com.android.developers.androidify.theme.AndroidifyTheme
136131
import com.android.developers.androidify.theme.LimeGreen
137132
import com.android.developers.androidify.theme.LocalSharedTransitionScope
@@ -160,32 +155,41 @@ import com.android.developers.androidify.creation.R as CreationR
160155

161156
@Composable
162157
fun CreationScreen(
163-
fileName: String? = null,
164-
creationViewModel: CreationViewModel = hiltViewModel(),
158+
creationViewModel: CreationViewModel,
165159
isMedium: Boolean = isAtLeastMedium(),
166160
onCameraPressed: () -> Unit = {},
167161
onBackPressed: () -> Unit,
168162
onAboutPressed: () -> Unit,
163+
onImageCreated: (resultImageUri: Uri, prompt: String?, originalImageUri: Uri?) -> Unit,
169164
) {
170165
val uiState by creationViewModel.uiState.collectAsStateWithLifecycle()
171166
BackHandler(
172167
enabled = uiState.screenState != ScreenState.EDIT,
173168
) {
174169
creationViewModel.onBackPress()
175170
}
176-
LaunchedEffect(Unit) {
177-
if (fileName != null) {
178-
creationViewModel.onImageSelected(fileName.toUri())
179-
} else {
180-
creationViewModel.onImageSelected(null)
181-
}
182-
}
183171
val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
184172
if (uri != null) {
185173
creationViewModel.onImageSelected(uri)
186174
}
187175
}
188176
val snackbarHostState by creationViewModel.snackbarHostState.collectAsStateWithLifecycle()
177+
178+
LaunchedEffect(uiState.resultBitmapUri) {
179+
uiState.resultBitmapUri?.let { resultBitmapUri ->
180+
onImageCreated(
181+
resultBitmapUri,
182+
uiState.descriptionText.text.toString(),
183+
if (uiState.selectedPromptOption == PromptType.PHOTO) {
184+
uiState.imageUri
185+
} else {
186+
null
187+
},
188+
)
189+
creationViewModel.onResultDisplayed()
190+
}
191+
}
192+
189193
when (uiState.screenState) {
190194
ScreenState.EDIT -> {
191195
EditScreen(
@@ -213,46 +217,6 @@ fun CreationScreen(
213217
},
214218
)
215219
}
216-
217-
ScreenState.RESULT -> {
218-
val prompt = uiState.descriptionText.text.toString()
219-
val key = if (uiState.descriptionText.text.isBlank()) {
220-
uiState.imageUri.toString()
221-
} else {
222-
prompt
223-
}
224-
ResultsScreen(
225-
uiState.resultBitmap!!,
226-
if (uiState.selectedPromptOption == PromptType.PHOTO) {
227-
uiState.imageUri
228-
} else {
229-
null
230-
},
231-
promptText = prompt,
232-
viewModel = hiltViewModel(key = key),
233-
onAboutPress = onAboutPressed,
234-
onBackPress = onBackPressed,
235-
onNextPress = creationViewModel::customizeExportClicked,
236-
)
237-
}
238-
239-
ScreenState.CUSTOMIZE -> {
240-
val prompt = uiState.descriptionText.text.toString()
241-
val key = if (uiState.descriptionText.text.isBlank()) {
242-
uiState.imageUri.toString()
243-
} else {
244-
prompt
245-
}
246-
uiState.resultBitmap?.let { bitmap ->
247-
CustomizeAndExportScreen(
248-
resultImage = bitmap,
249-
originalImageUri = uiState.imageUri,
250-
onBackPress = onBackPressed,
251-
onInfoPress = onAboutPressed,
252-
viewModel = hiltViewModel<CustomizeExportViewModel>(key = key),
253-
)
254-
}
255-
}
256220
}
257221
}
258222

0 commit comments

Comments
 (0)