0

In the Add Shopping List item screen, I have a Modal Sheet with options to take a camera snapshot or choose an image from the gallery. In this screen, I'm prepopulating Textfields either from a shared ViewModel or from Navigation arguments. The issue I'm having is that when I take a photo or pick from the gallery, sometimes the LaunchedEffect code block randomly executes and causes the TextFields to be reset. However, the LaunchedEffect always executes for the first time upon the composable entering the composition, as intended. What could be wrong with the code?

Main Activity NavHost

@Composable
fun ShoppingListApp(navController: NavHostController) {
    LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
    val sharedViewModel: SharedViewModel = hiltViewModel()
    AnimatedNavHost(
        navController = navController, startDestination = NavScreens.MainScreen.route,
        enterTransition = { EnterTransition.None },
        exitTransition = { ExitTransition.None }
    ) {
        composable(
            route = NavScreens.MainScreen.route,
        ) {
            MainScreen(navController)
        }

        composable(
            route = NavScreens.ShoppingListScreen.route
        ) {
            val viewModel: ShoppingListScreenViewModel = hiltViewModel()
            ShoppingListScreen(navController, viewModel, sharedViewModel)
        }

        composable(
            route = NavScreens.AddItemScreen.route + "?id={id}&name={name}&category={category}",
            arguments = listOf(
                navArgument("id") {
                    type = NavType.LongType
                    defaultValue = 0L
                },
                navArgument("name") {
                    type = NavType.StringType
                    defaultValue = ""
                },
                navArgument("category") {
                    type = NavType.StringType
                    defaultValue = ""
                }
            )
        ) { entry ->
            val addShoppingListScreenviewModel: AddShoppingListItemScreenViewModel = hiltViewModel()
            AddItemScreen(
                name = entry.arguments?.getString("name")!!,
                category = entry.arguments?.getString("category")!!,
                navController = navController,
                addShoppingListItemScreenViewModel = addShoppingListScreenviewModel,
                sharedViewModel = sharedViewModel
            )
        }
    }
}

Shared ViewModel

class SharedViewModel: ViewModel() {
    private val _item = mutableStateOf(ShoppingListItem())
    private val _isEdit = mutableStateOf(false)

    val item: State<ShoppingListItem>
        get() = _item

    val isEdit: State<Boolean>
        get() = _isEdit

    fun setState(stateToEdit: String, stateValue: Any?){
        var item = _item.value
        var isEdit = _isEdit.value

        when (stateToEdit) {
            "ShoppingListItem" -> item = stateValue as ShoppingListItem
            "IsEdit" -> isEdit = stateValue as Boolean
        }
        _item.value = item
        _isEdit.value = isEdit
    }
}

Add Item Composable

@Composable
fun AddItemScreen(
    navController: NavHostController,
    addShoppingListItemScreenViewModel: AddShoppingListItemScreenViewModel,
    sharedViewModel: SharedViewModel,
    name: String = "",
    category: String = "",
) {
    val shoppingListItem = sharedViewModel.item.value

    LaunchedEffect(Unit) {
        if (name.isNotEmpty() && category.isNotEmpty()) {
            addShoppingListItemScreenViewModel.setStateValue("Name", name)
            addShoppingListItemScreenViewModel.setStateValue("Category", category)
        } else {
            addShoppingListItemScreenViewModel.setStateValue(
                "ShoppingListItem",
                shoppingListItem
            )
        }
    }
...
}
Raj Narayanan
  • 2,443
  • 4
  • 24
  • 43
  • 3
    `LaunchedEffect` will launch every time it is added to composition, yes. Why are you using a `LaunchedEffect` at all when [navigation arguments can already be sent directly to your `ViewModel`](https://stackoverflow.com/a/67352130/1676363)? – ianhanniballake Jul 29 '22 at 03:17
  • `LaunchedEffect(Unit)` executes every recomposition of composable. If you want to it to execute only one time, use `LaunchedEffect(true)` – Vygintas B Jul 29 '22 at 07:12
  • @VygintasB This is not true. Both your examples behaves exactly the same. They are both executed when added to composition – Jakoss Jul 29 '22 at 07:19
  • @Jakoss Yes, they are both executed on composition, but not on every recomposition. Correct me if I'm wrong – Vygintas B Jul 29 '22 at 11:14
  • @VygintasB `LaunchedEffect(true)` didn't fix the random executions I'm having. – Raj Narayanan Jul 29 '22 at 11:36
  • @VygintasB LaunchedEffect is run on initial composition and every change of it's parameter. Unit won't change, ever. Just like true. So both those are working exactly the same – Jakoss Jul 29 '22 at 11:52
  • 1
    @rajndev When you start another Activity such as taking a photo or picking from gallery, your currently active Activity might get briefly killed when the other activity opens and is then recreated when you navigate back, that is why you only see this behavior "sometimes". When that happens your VMs should retain the state, but since you are combining it with navigation parameters, if your state already changed, then the nav parameters might reset it (I did not check all your logic). So my recommendation would be to debug the logic by running your app in Debug mode and place a few breakpoints. – Ma3x Jul 30 '22 at 07:30
  • 1
    I also see that you are locking the orientation, but that still does not prevent other configuration changes from restarting your Activity and thus re-running your whole compose code including all `LaunchedEffect(Unit)` when a configuration change happens. If you want to debug this realiably (i.e. not rely on "sometimes"), you can disable the orientataion lock while debugging and simply rotate the device. That will recreate your Activity and you can then debug and change the code and state to be resistant to Activity recreation. If you fix that, then the "sometimes" bug should be gone too. – Ma3x Jul 30 '22 at 07:38
  • @Ma3x This is a good tip! :) – Raj Narayanan Jul 30 '22 at 13:21
  • @ianhanniballake This only works for primitive types passed as Nav Arguments, although flawed solutions exist to pass objects. For the `ShoppingListItem` object, I just retrieved the item from the database in the viewmodel of the other end with the `id`passed in as a Nav Argument. :) – Raj Narayanan Jul 31 '22 at 16:10
  • 1
    @rajndev - Navigation supports passing [any custom type](https://developer.android.com/guide/navigation/navigation-kotlin-dsl#custom-types), but you're right that you should absolutely use your repository as the single source of truth that both destinations get their data from. – ianhanniballake Jul 31 '22 at 17:44

0 Answers0