Skip to content
Next Next commit
Moved ResultsScreen.kt & CustomizeExportScreen.kt to open from MainNa…
…vigation.kt using Nav3 instead of driving them from CreationScreen.kt

# Conflicts:
#	feature/results/src/main/java/com/android/developers/androidify/customize/ImageRenderer.kt

# Conflicts:
#	feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt
  • Loading branch information
srikrishnasakunia committed Aug 17, 2025
commit df7c28c858d3edcf65fb435920f6badbd507f13e
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import androidx.core.net.toUri
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.customize.CustomizeAndExportScreen
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.theme.transitions.ColorSplashTransitionScreen
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity

Expand Down Expand Up @@ -110,6 +113,51 @@ fun MainNavigation() {
onAboutPressed = {
backStack.add(About)
},
onImageCreated = { resultImageUri, prompt, originalImageUri ->
backStack.removeAll{ it is ImageResult}
backStack.add(
ImageResult(
result = resultImageUri.toString(),
prompt = prompt,
originalImageUri = originalImageUri?.toString()
)
)
}
)
}
entry<ImageResult> { resultKey ->
ResultsScreen(
resultImageUri = resultKey.result.toUri(),
originalImageUri = resultKey.originalImageUri?.toUri(),
onNextPress = { resultImageUri, originalImageUri ->
backStack.removeAll{ it is ImageResult}
backStack.add(
ShareResult(
resultUri = resultImageUri.toString(),
originalImageUri = originalImageUri?.toString()
)
)
},
promptText = resultKey.prompt,
onAboutPress = {
backStack.add(About)
},
onBackPress = {
backStack.removeLastOrNull()
backStack.add(Create(fileName = resultKey.originalImageUri, prompt = resultKey.prompt))
}
)
}
entry<ShareResult> { shareKey ->
CustomizeAndExportScreen(
resultImageUri = shareKey.resultUri.toUri(),
originalImageUri = shareKey.originalImageUri?.toUri(),
onBackPress = {
backStack.removeLastOrNull()
},
onInfoPress = {
backStack.add(About)
}
)
}
entry<About> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ object Camera : NavigationRoute

@Serializable
object About : NavigationRoute

@Serializable
data class ImageResult(val originalImageUri: String? = null, val prompt: String? = null, val result: String) : NavigationRoute

@Serializable
data class ShareResult(val resultUri: String, val originalImageUri: String?) : NavigationRoute
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ fun CreationScreen(
onCameraPressed: () -> Unit = {},
onBackPressed: () -> Unit,
onAboutPressed: () -> Unit,
onImageCreated: (resultImageUri: Uri, prompt: String?, originalImageUri: Uri?) -> Unit,
) {
val uiState by creationViewModel.uiState.collectAsStateWithLifecycle()
BackHandler(
Expand Down Expand Up @@ -213,44 +214,16 @@ 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!!,
onImageCreated(
uiState.resultBitmapUri!!,
uiState.descriptionText.text.toString(),
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ import com.android.developers.androidify.data.TextGenerationRepository
import com.android.developers.androidify.util.LocalFileProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -153,7 +157,7 @@ class CreationViewModel @Inject constructor(
)
}
_uiState.update {
it.copy(resultBitmap = bitmap, screenState = ScreenState.RESULT)
it.copy(resultBitmapUri = saveBitmapToCache(context, bitmap), screenState = ScreenState.RESULT)
}
} catch (e: Exception) {
handleImageGenerationError(e)
Expand Down Expand Up @@ -220,25 +224,13 @@ class CreationViewModel @Inject constructor(

ScreenState.RESULT -> {
_uiState.update {
it.copy(screenState = ScreenState.EDIT, resultBitmap = null)
it.copy(screenState = ScreenState.EDIT, resultBitmapUri = null)
}
}

ScreenState.EDIT -> {
// do nothing, back press handled outside
}

ScreenState.CUSTOMIZE -> {
_uiState.update {
it.copy(screenState = ScreenState.RESULT)
}
}
}
}

fun customizeExportClicked() {
_uiState.update {
it.copy(screenState = ScreenState.CUSTOMIZE)
}
}
}
Expand All @@ -252,14 +244,13 @@ data class CreationState(
val generatedPrompt: String? = null,
val promptGenerationInProgress: Boolean = false,
val screenState: ScreenState = ScreenState.EDIT,
val resultBitmap: Bitmap? = null,
val resultBitmapUri: Uri? = null,
)

enum class ScreenState {
EDIT,
LOADING,
RESULT,
CUSTOMIZE,
}

data class BotColor(
Expand Down Expand Up @@ -301,3 +292,24 @@ enum class PromptType(val displayName: String) {
PHOTO("Photo"),
TEXT("Prompt"),
}

suspend fun saveBitmapToCache(
context: Context,
bitmap: Bitmap,
compressionFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): Uri? = withContext(Dispatchers.IO) {

val cacheDir = context.cacheDir
val fileName = File(cacheDir, "temp_image_${System.currentTimeMillis()}.jpg")
try {
FileOutputStream(fileName).use { outputStream ->
bitmap.compress(compressionFormat, quality, outputStream)
}
Uri.fromFile(fileName)
} catch (e: Exception) {
e.printStackTrace()
null
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class CreationViewModelTest {
viewModel.onSelectedPromptOptionChanged(PromptType.PHOTO)
viewModel.startClicked()
assertEquals(ScreenState.RESULT, viewModel.uiState.value.screenState)
assertNotNull(viewModel.uiState.value.resultBitmap)
assertNotNull(viewModel.uiState.value.resultBitmapUri)
}

@Test
Expand Down Expand Up @@ -178,7 +178,7 @@ class CreationViewModelTest {
}
viewModel.startClicked()
assertEquals(ScreenState.RESULT, viewModel.uiState.value.screenState)
assertNotNull(viewModel.uiState.value.resultBitmap)
assertNotNull(viewModel.uiState.value.resultBitmapUri)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ class ResultsScreenTest {
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

// Create a dummy bitmap for testing
private val testBitmap: Bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val dummyUri = android.net.Uri.parse("dummy://image")

@Test
fun resultsScreenContents_displaysActionButtons() {
val shareButtonText = composeTestRule.activity.getString(R.string.customize_and_share)
// Note: Download button is identified by icon, harder to test reliably without tags/desc

val initialState = ResultState(resultImageBitmap = testBitmap, promptText = "test")
val initialState = ResultState(resultImageUri = dummyUri, promptText = "test")
val state = mutableStateOf(initialState)

composeTestRule.setContent {
Expand Down Expand Up @@ -78,7 +78,7 @@ class ResultsScreenTest {
val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot)

// Ensure promptText is non-null when bitmap is present
val initialState = ResultState(resultImageBitmap = testBitmap, promptText = "test")
val initialState = ResultState(resultImageUri = dummyUri, promptText = "test")
val state = mutableStateOf(initialState)

composeTestRule.setContent {
Expand Down Expand Up @@ -108,7 +108,7 @@ class ResultsScreenTest {
val backCardDesc = composeTestRule.activity.getString(R.string.original_image)
val dummyUri = android.net.Uri.parse("dummy://image")

val initialState = ResultState(resultImageBitmap = testBitmap, originalImageUrl = dummyUri)
val initialState = ResultState(resultImageUri = dummyUri, originalImageUrl = dummyUri)
val state = mutableStateOf(initialState)

composeTestRule.setContent {
Expand Down Expand Up @@ -143,7 +143,7 @@ class ResultsScreenTest {
val promptText = "test prompt"
val promptPrefix = composeTestRule.activity.getString(R.string.my_bot_is_wearing)

val initialState = ResultState(resultImageBitmap = testBitmap, promptText = promptText) // No original image URI
val initialState = ResultState(resultImageUri = dummyUri, promptText = promptText) // No original image URI
val state = mutableStateOf(initialState)

composeTestRule.setContent {
Expand Down Expand Up @@ -175,7 +175,7 @@ class ResultsScreenTest {
val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot)
val dummyUri = android.net.Uri.parse("dummy://image")

val initialState = ResultState(resultImageBitmap = testBitmap, originalImageUrl = dummyUri)
val initialState = ResultState(resultImageUri = dummyUri, originalImageUrl = dummyUri)
val state = mutableStateOf(initialState)

composeTestRule.setContent {
Expand Down Expand Up @@ -210,7 +210,7 @@ class ResultsScreenTest {
var shareClicked = false

// Ensure promptText is non-null when bitmap is present
val initialState = ResultState(resultImageBitmap = testBitmap, promptText = "test")
val initialState = ResultState(resultImageUri = dummyUri, promptText = "test")
val state = mutableStateOf(initialState)

composeTestRule.setContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.LookaheadScope
Expand All @@ -72,6 +71,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.ui.LocalNavAnimatedContentScope
Expand All @@ -97,15 +97,15 @@ import com.android.developers.androidify.theme.R as ThemeR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomizeAndExportScreen(
resultImage: Bitmap,
resultImageUri: Uri,
originalImageUri: Uri?,
onBackPress: () -> Unit,
onInfoPress: () -> Unit,
isMediumWindowSize: Boolean = isAtLeastMedium(),
viewModel: CustomizeExportViewModel = hiltViewModel<CustomizeExportViewModel>(),
) {
LaunchedEffect(resultImage, originalImageUri) {
viewModel.setArguments(resultImage, originalImageUri)
LaunchedEffect(resultImageUri, originalImageUri) {
viewModel.setArguments(resultImageUri, originalImageUri)
}
val state = viewModel.state.collectAsStateWithLifecycle()
val context = LocalContext.current
Expand Down Expand Up @@ -430,9 +430,9 @@ fun CustomizeExportPreview() {
AnimatedContent(true) { targetState ->
targetState
CompositionLocalProvider(LocalNavAnimatedContentScope provides this@AnimatedContent) {
val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot)
val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri()
val state = CustomizeExportState(
exportImageCanvas = ExportImageCanvas(imageBitmap = bitmap.asAndroidBitmap()),
exportImageCanvas = ExportImageCanvas(imageUri = imageUri),
)
CustomizeExportContents(
state = state,
Expand All @@ -457,10 +457,10 @@ fun CustomizeExportPreviewLarge() {
AnimatedContent(true) { targetState ->
targetState
CompositionLocalProvider(LocalNavAnimatedContentScope provides this@AnimatedContent) {
val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot)
val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri()
val state = CustomizeExportState(
exportImageCanvas = ExportImageCanvas(
imageBitmap = bitmap.asAndroidBitmap(),
imageUri = imageUri,
aspectRatioOption = SizeOption.Square,
),
selectedTool = CustomizeTool.Background,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ class CustomizeExportViewModel @Inject constructor(
}

fun setArguments(
resultImageUrl: Bitmap,
resultImageUrl: Uri,
originalImageUrl: Uri?,
) {
_state.update {
CustomizeExportState(
originalImageUrl,
exportImageCanvas = it.exportImageCanvas.copy(imageBitmap = resultImageUrl),
exportImageCanvas = it.exportImageCanvas.copy(imageUri = resultImageUrl),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is where its losing state on restoring from background. we can keep the imageUri, but we should probably add the loaded image to the State to be able to draw it.
So instead of changing the imageBitmap to imageUri, lets add an imageUri parameter, and then on load of the bitmap, we set the state to that Bitmap.

When it comes back from being in the background, we will need to rehydrate the state to load up the bitmap again.

)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The setArguments function appears to be redundant now that the ViewModel's initial state is configured through assisted injection in the init block. This function is only used in tests and its logic is a subset of the init block's. To avoid confusion and potential misuse, it's best to remove it and update the tests to rely solely on ViewModel instantiation for setup.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ data class BackgroundToolState(
) : ToolState

data class ExportImageCanvas(
val imageBitmap: Bitmap? = null,
val imageUri: Uri? = null,
val imageBitmapRemovedBackground: Bitmap? = null,
val aspectRatioOption: SizeOption = SizeOption.Square,
val canvasSize: Size = Size(1000f, 1000f),
Expand Down
Loading