Skip to content

Comments

Fix to #1046#1053

Closed
devmike01 wants to merge 11 commits intoandroid:mainfrom
devmike01:fix/show_task_message_once
Closed

Fix to #1046#1053
devmike01 wants to merge 11 commits intoandroid:mainfrom
devmike01:fix/show_task_message_once

Conversation

@devmike01
Copy link

Fixes to creating, adding or editing a task shows the snackbar more than once when navigating.

@gemini-code-assist
Copy link

Summary of Changes

Hello @devmike01, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves an issue where snackbar messages, intended to provide one-time feedback for task operations, would repeatedly appear upon navigation. The solution introduces a robust AppMessageQueue that stores and retrieves these messages, ensuring they are consumed and displayed only once. This change significantly improves the user experience by eliminating redundant notifications and enhances the overall stability of the application's message handling system.

Highlights

  • New Message Queuing System: Implemented a new AppMessageQueue mechanism using SharedPreferences to manage one-time messages, such as snackbar notifications, ensuring they are displayed only once.
  • Refactored Message Handling: Updated AddEditTaskScreen, TaskDetailScreen, and TasksScreen to utilize the new AppMessageQueue for displaying success/failure messages, moving away from passing messages via navigation arguments.
  • Enhanced Navigation Logic: Modified navigation actions in TodoNavGraph and TodoNavigation to include specific flags (popUpTo, launchSingleTop, restoreState) that prevent snackbar messages from reappearing upon navigation events.
  • Comprehensive UI Testing: Added new UI tests in TasksScreenTest to rigorously verify that snackbar messages are displayed exactly once after task creation, editing, or deletion, and subsequent navigation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt
    • Added taskId = null to AddEditTaskScreen test setup for consistency.
  • app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt
    • Introduced TestNavHostController, TodoNavigationActions, and CountingIdlingResource for advanced UI testing scenarios.
    • Added a new UI test snackBarOnlyDisplayOnce() to specifically verify the single display of snackbar messages after task operations and navigation.
    • Refactored setContent() to use a TestNavHost for improved navigation testing.
    • Added TaskTestViewModelFactory and AddEditTaskViewModelFactory for test ViewModel instantiation.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt
    • Added android.content.Context import, likely for AppMessageQueue setup.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt
    • Removed userMessage argument from TasksScreen composable and its associated navArgument definition.
    • Modified AddEditTaskScreen and TaskDetailScreen navigation callbacks to use new navigateToTasksV2() and navigateToTasks() functions without result codes.
    • Added NO_MESSAGE constant for explicit message handling.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt
    • Introduced navigateToTasks() and navigateToTasksV2() functions with specific navigation options (popUpTo, launchSingleTop, restoreState) to control the back stack and prevent message re-display.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt
    • Added taskId: String? parameter to AddEditTaskScreen.
    • Integrated viewModel.setPendingMessage() to store task update results in the AppMessageQueue.
    • Added testTag modifiers to UI elements (FAB, title, description fields) for improved testability.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt
    • Added setPendingMessage() function to interact with the TaskRepository for message queuing.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt
    • Injected AppMessageQueue into the constructor.
    • Implemented addPendingMessage() and readOncePendingMessage() by delegating to the AppMessageQueue.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt
    • Added addPendingMessage() and readOncePendingMessage() to the TaskRepository interface.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt
    • Provided AppMessageQueue as a Dagger Hilt injectable using AppMessageQueueImpl backed by SharedPreferences.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt
    • Integrated viewModel.setPendingMessage() to store task deletion results in the AppMessageQueue.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt
    • Added setPendingMessage() function to interact with the TaskRepository for message queuing.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt
    • Removed userMessage parameter from TasksScreen.
    • Modified LaunchedEffect to trigger viewModel.showEditResultMessage() for displaying snackbars based on the AppMessageQueue content.
    • Added testTag to the add task Floating Action Button (FAB).
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt
    • Updated showEditResultMessage() to read messages from taskRepository.readOncePendingMessage() and handle NO_MESSAGE to prevent unintended snackbar displays.
  • app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/AppMessageQueue.kt
    • New file defining the AppMessageQueue interface and its AppMessageQueueImpl implementation, which uses SharedPreferences for one-time message storage and retrieval.
  • app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt
    • Updated DefaultTaskRepository test instantiation to include FakeAppMessageQueue for isolated testing.
  • app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt
    • Modified tests for showEditResultMessages to align with the new message queuing mechanism, ensuring messages are added to the repository before being read by the ViewModel.
  • shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeAppMessageQueue.kt
    • New file providing a test implementation of AppMessageQueue using an in-memory map.
  • shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeTaskRepository.kt
    • Integrated FakeAppMessageQueue to support testing of message queuing within the fake repository.
Activity
  • The author devmike01 initiated this pull request to address a bug where snackbar messages were being displayed multiple times after task operations and navigation.
  • The core of the solution involved introducing a new AppMessageQueue system to manage one-time messages, ensuring they are consumed and displayed only once.
  • This required significant refactoring across the application's architecture, including UI components (Screens), ViewModels, the TaskRepository, and the Dependency Injection setup.
  • Navigation logic was updated to prevent message re-triggering due to navigation events.
  • New, comprehensive UI tests were added to TasksScreenTest to validate the fix, specifically confirming that snackbars appear only once.
  • Test utilities, such as FakeAppMessageQueue, were created to facilitate robust unit testing of the new message handling mechanism.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a solid fix for a bug where snackbars were displayed multiple times. The new approach using a persistent, read-once message queue (AppMessageQueue) is a robust solution. The changes are well-supported by a new integration test that validates the fix. However, I've identified a few issues that should be addressed, including a functional bug causing incorrect snackbar messages, some code duplication, a potential performance issue with SharedPreferences, and a couple of areas for improvement in code clarity and test robustness.

@thewyp
Copy link

thewyp commented Feb 6, 2026 via email

@devmike01 devmike01 marked this pull request as draft February 6, 2026 14:16
@devmike01 devmike01 marked this pull request as ready for review February 6, 2026 14:19
@devmike01
Copy link
Author

devmike01 commented Feb 6, 2026

@gemini-code-assist /gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively resolves the issue of snackbars appearing multiple times by implementing a SharedPreferences-backed message queue. This ensures that user messages are consumed only once, even across navigation changes. The refactoring of the navigation logic to support this and the addition of a new UI test to verify the fix are excellent improvements. I've included a few suggestions to enhance performance, clean up test code, and remove some now-obsolete code.

}

override fun produce(message: Int) {
prefs.edit(commit = true){ putInt(MESSAGE_KEY, message) }

Choose a reason for hiding this comment

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

high

Using commit = true with SharedPreferences.edit() performs a synchronous write to disk, which can block the main thread and cause UI jank. Since this produce method is called from a LaunchedEffect, it runs on the main thread. It's better to use apply() for an asynchronous write. The androidx.core.content.edit extension function uses apply() by default (when commit is false).

Suggested change
prefs.edit(commit = true){ putInt(MESSAGE_KEY, message) }
prefs.edit { putInt(MESSAGE_KEY, message) }

.onNodeWithContentDescription("Save task")
.performClick()

snackBar.isDisplayed()

Choose a reason for hiding this comment

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

medium

The isDisplayed() method returns a boolean indicating if the node is displayed, but it doesn't perform any assertion. This line has no effect on the test result. To verify that the snackbar is displayed, you should use assertIsDisplayed().

Suggested change
snackBar.isDisplayed()
snackBar.assertIsDisplayed()

Comment on lines 396 to 407
class TaskTestViewModelFactory(private val repository: TaskRepository)
: ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TasksViewModel(repository, SavedStateHandle()) as T
}
}

class AddEditTaskViewModelFactory(private val repository: TaskRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AddEditTaskViewModel(repository, SavedStateHandle()) as T
}
} No newline at end of file

Choose a reason for hiding this comment

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

medium

These ViewModelProvider.Factory classes (TaskTestViewModelFactory and AddEditTaskViewModelFactory) appear to be unused in the test file. Since Hilt is used for dependency injection, these factories are not needed and can be removed to keep the code clean.

AppModalDrawer(drawerState, currentRoute, navActions) {
TasksScreen(
userMessage = entry.arguments?.getInt(USER_MESSAGE_ARG)!!,
onUserMessageDisplayed = { entry.arguments?.putInt(USER_MESSAGE_ARG, 0) },

Choose a reason for hiding this comment

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

medium

The onUserMessageDisplayed callback seems to be dead code now. It attempts to modify USER_MESSAGE_ARG in the navigation entry's arguments, but this argument is no longer part of the tasks route since the navigation logic was updated. This callback, its parameter in TasksScreen, and its call inside TasksScreen can be removed for better code clarity and maintainability.

Comment on lines 77 to 80




Choose a reason for hiding this comment

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

medium

There are excessive blank lines here, which affect code readability. It's good practice to remove them.

@devmike01
Copy link
Author

@gemini-code-assist /gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request successfully fixes the issue of snackbars appearing multiple times by refactoring how user messages are passed between screens. Instead of using navigation arguments, it introduces an AppMessageQueue backed by SharedPreferences to handle one-shot messages. The changes are well-tested with a new integration test that verifies the fix.

However, I've found a couple of issues. There's a regression in the navigation logic that affects state restoration when using the navigation drawer. Additionally, there's a minor performance concern in the new AppMessageQueue implementation due to synchronous disk I/O on the main thread. My detailed comments provide suggestions to address these points.

Comment on lines 64 to 75
fun navigateToTasks() {
navController.navigate(
TASKS_SCREEN.let {
if (userMessage != 0) "$it?$USER_MESSAGE_ARG=$userMessage" else it
}
TASKS_SCREEN
) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = !navigatesFromDrawer
saveState = navigatesFromDrawer
inclusive = true
saveState = false
}
launchSingleTop = true
restoreState = navigatesFromDrawer
restoreState = false
}
}

Choose a reason for hiding this comment

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

high

This refactoring of navigateToTasks fixes the snackbar issue but introduces a regression. The TasksScreen state (like scroll position and filter) will no longer be saved and restored when navigating from the drawer, because saveState and restoreState are now always false.

The previous implementation correctly handled two cases: navigating from the drawer (preserving state) and navigating after an action like adding/editing a task (clearing state).

To fix this, you should have two distinct navigation actions: one for navigating from the drawer (which should save state) and one for navigating after an action (which should clear the back stack as you've done here).

The logic for drawer navigation should be similar to navigateToStatistics, which correctly uses saveState = true and restoreState = true.

}

override fun produce(message: Int) {
prefs.edit(commit = true){ putInt(MESSAGE_KEY, message) }

Choose a reason for hiding this comment

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

medium

Using commit = true with SharedPreferences.edit() performs a synchronous write to disk, which can block the main thread and potentially cause Application Not Responding (ANR) errors. Since this is called from a LaunchedEffect, it runs on the main thread.

It's recommended to use apply() for asynchronous writes. The edit KTX function uses apply() by default. You can simply remove commit = true.

Suggested change
prefs.edit(commit = true){ putInt(MESSAGE_KEY, message) }
prefs.edit { putInt(MESSAGE_KEY, message) }

@devmike01
Copy link
Author

@gemini-code-assist /gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request effectively addresses the issue of snackbars displaying multiple times by introducing an AppMessageQueue mechanism. This is a robust solution for managing transient UI messages across navigation. The changes are well-integrated into the existing architecture, updating navigation logic, ViewModel interactions, and including new test cases to verify the fix. Overall, this is a good improvement to the application's message handling.

LaunchedEffect(snackbarHostState, viewModel, message, snackbarText) {
snackbarHostState.showSnackbar(snackbarText)
viewModel.snackbarMessageShown()
// viewModel.snackbarMessageShown()

Choose a reason for hiding this comment

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

high

The viewModel.snackbarMessageShown() call is commented out here. This means that once a snackbar message is displayed, the ViewModel's internal state for userMessage will not be cleared. This could lead to the same message being re-displayed unexpectedly if the screen recomposes or if the user navigates away and back.

Suggested change
// viewModel.snackbarMessageShown()
viewModel.snackbarMessageShown()



@Composable
fun TestNavHost(navController: NavHostController, repository: TaskRepository){

Choose a reason for hiding this comment

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

medium

The repository parameter in TestNavHost is not used. It's good practice to remove unused parameters to keep the code clean and avoid confusion.

Suggested change
fun TestNavHost(navController: NavHostController, repository: TaskRepository){
fun TestNavHost(navController: NavHostController){


package com.example.android.architecture.blueprints.todoapp

import androidx.compose.ui.window.Popup

Choose a reason for hiding this comment

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

medium

The import for androidx.compose.ui.window.Popup is not used in this file. Please remove unused imports to maintain code cleanliness.

}

fun navigateToStatistics() {
fun navigateToStatistics() {

Choose a reason for hiding this comment

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

medium

There's an extra tab indentation for the navigateToStatistics function declaration. Please align it with the class definition.

Suggested change
fun navigateToStatistics() {
fun navigateToStatistics() {

import com.example.android.architecture.blueprints.todoapp.util.Async
import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay

Choose a reason for hiding this comment

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

medium

The import kotlinx.coroutines.delay is not used in this file. Please remove unused imports.

coroutineScope: CoroutineScope = rememberCoroutineScope(),
content: @Composable () -> Unit
) {
val context = LocalContext.current

Choose a reason for hiding this comment

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

medium

The context variable is declared but not used within the AppModalDrawer composable. Please remove it if it's not needed.

composeTestRule.waitForIdle()

val newTaskBtn = composeTestRule.onNodeWithTag("New Task")
val snackBar = composeTestRule.onNodeWithText("Task added")

Choose a reason for hiding this comment

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

medium

The string "Task added" is hardcoded. For better maintainability and localization, consider using a string resource.


composeTestRule.waitForIdle()

composeTestRule.onNodeWithTag("Title").performTextInput("TITLE1")

Choose a reason for hiding this comment

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

medium

The string "TITLE1" is hardcoded. For better maintainability and localization, consider using a string resource.

composeTestRule.waitForIdle()

composeTestRule.onNodeWithTag("Title").performTextInput("TITLE1")
composeTestRule.onNodeWithTag("Enter your task here.").performTextInput("DESCRIPTION1")

Choose a reason for hiding this comment

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

medium

The string "DESCRIPTION1" is hardcoded. For better maintainability and localization, consider using a string resource.

composeTestRule.onNodeWithContentDescription("Back")
.assertIsDisplayed()
.performClick()
composeTestRule.onNodeWithTag("Todo")

Choose a reason for hiding this comment

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

medium

The string "Todo" is hardcoded. For better maintainability and localization, consider using a string resource.

@devmike01
Copy link
Author

@gemini-code-assist /gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses the issue of duplicate snackbar messages by introducing an AppMessageQueue to manage one-time messages across navigation events. This is a robust solution for handling UI feedback after asynchronous operations and navigation. The addition of testTag modifiers in several composables is also a good practice for improving testability, and the updated tests confirm the new behavior.

@devmike01
Copy link
Author

@gemini-code-assist /gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively resolves the issue of snackbars appearing multiple times by introducing a message queue system (AppMessageQueue) to handle one-off user messages. This is a great improvement over passing messages via navigation arguments. The changes are well-implemented, and the inclusion of the snackBarOnlyDisplayOnce integration test is excellent for verifying the fix and preventing future regressions. My review includes a few suggestions to further improve code clarity and maintainability, such as using nullable types for the message queue, ensuring consistency in tests by using string resources, and extracting hardcoded strings into constants.

@devmike01 devmike01 marked this pull request as draft February 6, 2026 22:50
@devmike01 devmike01 marked this pull request as ready for review February 6, 2026 22:50
@devmike01 devmike01 closed this Feb 19, 2026
@devmike01 devmike01 deleted the fix/show_task_message_once branch February 19, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants