Skip to content

Migrate ResultsScreen to use Nav3 #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.android.developers.androidify.camera.CameraPreviewScreen
import com.android.developers.androidify.creation.CreationScreen
import com.android.developers.androidify.creation.CreationViewModel
import com.android.developers.androidify.customize.CustomizeAndExportScreen
import com.android.developers.androidify.customize.CustomizeExportViewModel
import com.android.developers.androidify.home.AboutScreen
import com.android.developers.androidify.home.HomeScreen
import com.android.developers.androidify.results.ResultsScreen
import com.android.developers.androidify.results.ResultsViewModel
import com.android.developers.androidify.theme.transitions.ColorSplashTransitionScreen
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity

Expand Down Expand Up @@ -92,14 +98,20 @@ fun MainNavigation() {
CameraPreviewScreen(
onImageCaptured = { uri ->
backStack.removeAll { it is Create }
backStack.add(Create(uri.toString()))
backStack.add(Create(uri))
backStack.removeAll { it is Camera }
},
)
}
entry<Create> { createKey ->
val creationViewModel = hiltViewModel<CreationViewModel, CreationViewModel.Factory>(
creationCallback = { factory ->
factory.create(
originalImageUrl = createKey.fileName
)
}
)
CreationScreen(
createKey.fileName,
onCameraPressed = {
backStack.removeAll { it is Camera }
backStack.add(Camera)
Expand All @@ -110,6 +122,65 @@ fun MainNavigation() {
onAboutPressed = {
backStack.add(About)
},
onImageCreated = { resultImageUri, prompt, originalImageUri ->
backStack.removeAll{ it is Result}
backStack.add(
Result(
resultImageUri = resultImageUri,
prompt = prompt,
originalImageUri = originalImageUri
)
)
},
creationViewModel = creationViewModel
)
}
entry<Result> { resultKey ->
val resultsViewModel = hiltViewModel<ResultsViewModel, ResultsViewModel.Factory>(
creationCallback = { factory ->
factory.create(
resultImageUrl = resultKey.resultImageUri,
originalImageUrl = resultKey.originalImageUri,
promptText = resultKey.prompt
)
}
)
ResultsScreen(
onNextPress = { resultImageUri, originalImageUri ->
backStack.removeAll{ it is Result}
backStack.add(
CustomizeExport(
resultImageUri = resultImageUri,
originalImageUri = originalImageUri
)
)
},
onAboutPress = {
backStack.add(About)
},
onBackPress = {
backStack.removeLastOrNull()
},
viewModel = resultsViewModel
)
}
entry<CustomizeExport> { shareKey ->
val customizeExportViewModel = hiltViewModel<CustomizeExportViewModel, CustomizeExportViewModel.Factory>(
creationCallback = { factory ->
factory.create(
resultImageUrl = shareKey.resultImageUri,
originalImageUrl = shareKey.originalImageUri
)
}
)
CustomizeAndExportScreen(
onBackPress = {
backStack.removeLastOrNull()
},
onInfoPress = {
backStack.add(About)
},
viewModel = customizeExportViewModel
)
}
entry<About> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package com.android.developers.androidify.navigation

import android.net.Uri
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable

Expand All @@ -26,10 +27,39 @@ sealed interface NavigationRoute
data object Home : NavigationRoute

@Serializable
data class Create(val fileName: String? = null, val prompt: String? = null) : NavigationRoute
data class Create(
@Serializable(with = UriSerializer::class) val fileName: Uri? = null,
val prompt: String? = null
) : NavigationRoute

@Serializable
object Camera : NavigationRoute

@Serializable
object About : NavigationRoute

/**
* Represents the result of an image generation process, used for navigation.
*
* @param resultImageUri The URI of the generated image.
* @param originalImageUri The URI of the original image used as a base for generation, if any.
* @param prompt The text prompt used to generate the image, if any.
*/
@Serializable
data class Result(
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I wonder if there is a way to register the UriSerializer for Nav3? @dturner

@Serializable(with = UriSerializer::class) val originalImageUri: Uri? = null,
val prompt: String? = null
) : NavigationRoute

/**
* Represents the navigation route to the screen for customizing and exporting a generated image.
*
* @param resultImageUri The URI of the generated image to be customized.
* @param originalImageUri The URI of the original image, passed along for context.
*/
@Serializable
data class CustomizeExport(
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
@Serializable(with = UriSerializer::class) val originalImageUri: Uri?
) : NavigationRoute
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.android.developers.androidify.navigation

import android.net.Uri
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import androidx.core.net.toUri

object UriSerializer: KSerializer<Uri> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: Uri) {
encoder.encodeString(value.toString())
}

override fun deserialize(decoder: Decoder): Uri = decoder.decodeString().toUri()
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ class TestFileProvider : LocalFileProvider {
): Uri {
TODO("Not yet implemented")
}

override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.android.developers.androidify.util
import android.app.Application
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
Expand Down Expand Up @@ -53,6 +54,9 @@ interface LocalFileProvider {

@WorkerThread
suspend fun saveUriToSharedStorage(inputUri: Uri, fileName: String, mimeType: String): Uri

@WorkerThread
suspend fun loadBitmapFromUri(uri: Uri): Bitmap?
}

@Singleton
Expand Down Expand Up @@ -120,6 +124,20 @@ class LocalFileProviderImpl @Inject constructor(
return@withContext newUri
}

override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
return withContext(ioDispatcher) {
try {
application.contentResolver.openInputStream(uri)?.use {
return@withContext BitmapFactory.decodeStream(it)
}
null
} catch (e: Exception) {
e.printStackTrace()
null
Comment on lines +134 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

e.printStackTrace() should be avoided in production code. It prints to the standard error stream and can be lost, especially in release builds where logs might be stripped. Using a structured logging framework like android.util.Log is a better practice for error handling as it allows for tagging and filtering of log messages.

Suggested change
} catch (e: Exception) {
e.printStackTrace()
null
} catch (e: Exception) {
android.util.Log.e("LocalFileProviderImpl", "Error loading bitmap from URI: $uri", e)
null
}

}
}
}

@Throws(IOException::class)
@WorkerThread
private fun saveFileToUri(file: File, uri: Uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.ripple
import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -123,6 +124,11 @@ import androidx.core.net.toUri
import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.rectangle
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
Expand Down Expand Up @@ -158,32 +164,41 @@ import com.android.developers.androidify.creation.R as CreationR

@Composable
fun CreationScreen(
fileName: String? = null,
creationViewModel: CreationViewModel = hiltViewModel(),
creationViewModel: CreationViewModel,
isMedium: Boolean = isAtLeastMedium(),
onCameraPressed: () -> Unit = {},
onBackPressed: () -> Unit,
onAboutPressed: () -> Unit,
onImageCreated: (resultImageUri: Uri, prompt: String?, originalImageUri: Uri?) -> Unit,
) {
val uiState by creationViewModel.uiState.collectAsStateWithLifecycle()
BackHandler(
enabled = uiState.screenState != ScreenState.EDIT,
) {
creationViewModel.onBackPress()
}
LaunchedEffect(Unit) {
if (fileName != null) {
creationViewModel.onImageSelected(fileName.toUri())
} else {
creationViewModel.onImageSelected(null)
}
}
val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
if (uri != null) {
creationViewModel.onImageSelected(uri)
}
}
val snackbarHostState by creationViewModel.snackbarHostState.collectAsStateWithLifecycle()

LaunchedEffect(uiState.resultBitmapUri) {
uiState.resultBitmapUri?.let { resultBitmapUri ->
onImageCreated(
resultBitmapUri,
uiState.descriptionText.text.toString(),
if (uiState.selectedPromptOption == PromptType.PHOTO) {
uiState.imageUri
} else {
null
}
)
creationViewModel.onResultDisplayed()
}
}

when (uiState.screenState) {
ScreenState.EDIT -> {
EditScreen(
Expand Down Expand Up @@ -211,46 +226,6 @@ fun CreationScreen(
},
)
}

ScreenState.RESULT -> {
val prompt = uiState.descriptionText.text.toString()
val key = if (uiState.descriptionText.text.isBlank()) {
uiState.imageUri.toString()
} else {
prompt
}
ResultsScreen(
uiState.resultBitmap!!,
if (uiState.selectedPromptOption == PromptType.PHOTO) {
uiState.imageUri
} else {
null
},
promptText = prompt,
viewModel = hiltViewModel(key = key),
onAboutPress = onAboutPressed,
onBackPress = onBackPressed,
onNextPress = creationViewModel::customizeExportClicked,
)
}

ScreenState.CUSTOMIZE -> {
val prompt = uiState.descriptionText.text.toString()
val key = if (uiState.descriptionText.text.isBlank()) {
uiState.imageUri.toString()
} else {
prompt
}
uiState.resultBitmap?.let { bitmap ->
CustomizeAndExportScreen(
resultImage = bitmap,
originalImageUri = uiState.imageUri,
onBackPress = onBackPressed,
onInfoPress = onAboutPressed,
viewModel = hiltViewModel<CustomizeExportViewModel>(key = key),
)
}
}
}
}

Expand Down
Loading
Loading