How to ensure data refresh in a Composable when returning to it in Jetpack Compose Navigation?
Asked Answered
R

3

7

I have a Composable which needs to fetch some data from my local Room Database when called. When I navigate to another Composable and then navigate back it should fetch the data again.

I fetch the data in a LaunchedEffect Block. And I'm using the Compose Destinations Library to navigate.

@Composable
fun DataScreen(navigator: DestinationsNavigator) {
    val viewModel = viewModel<DataScreenViewModel>()
    LaunchedEffect(Unit) {
        viewModel.fetchData()
    }

    Button(onClick = { navigator.navigate(EditScreenDestination) }) {
        Text("Edit")
    }
}

Now when I navigate away from this Composable and navigate back to it using navigator.popBackStack() I want to refetch the Data because it might have been changed.

Is there a way I can detect that the user navigated back in my DataScreen Composable?
Or is there a better way to achieve this.

Rarely answered 3/6, 2023 at 15:42 Comment(4)
the way you do it now should also work as the LaunchedEffect will be launched again when the composable is recomposedTalia
Why isn't your data layer already providing a reactive stream of data (e.g., a Kotlin Flow) that would mean that every layer automatically updates when the data changes? You shouldn't ever be manually refreshing things...Vaclava
@Vaclava Ah interesting I didn't know about Kotlin Flows. Apparantly there is also an integration of Flow with Room where the DAO returns a Flow which will emit new values every time the database is updated. I will try that out.Rarely
@zaidkhan oh you are right. I checked again and the LaunchedEffect does indeed get executed. Then I must have made a mistake somwhere else in my code.Rarely
T
1

Add this helper to your project:

package xxxxxxxxxxxx
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.Navigator

/**
 * The navigation result callback between two call screens.
 */
typealias NavResultCallback<T> = (T) -> Unit

// A SavedStateHandle key is used to set/get NavResultCallback<T>
private const val NavResultCallbackKey = "NavResultCallbackKey"

/**
 * Set the navigation result callback on calling screen.
 *
 * @param callback The navigation result callback.
 */
fun <T> NavController.setNavResultCallback(callback: NavResultCallback<T>) {
    currentBackStackEntry?.savedStateHandle?.set(NavResultCallbackKey, callback)
}

/**
 *  Get the navigation result callback on called screen.
 *
 * @return The navigation result callback if the previous backstack entry exists
 */
fun <T> NavController.getNavResultCallback() : NavResultCallback<T>? {
    return previousBackStackEntry?.savedStateHandle?.remove(NavResultCallbackKey)
}

/**
 *  Attempts to pop the controller's back stack and returns the result.
 *
 * @param result the navigation result
 */
fun <T> NavController.popBackStackWithResult(result: T) {
    getNavResultCallback<T>()?.invoke(result)
    popBackStack()
}

/**
 * Navigate to a route in the current NavGraph. If an invalid route is given, an
 * [IllegalArgumentException] will be thrown.
 *
 * @param route route for the destination
 * @param navResultCallback the navigation result callback
 * @param navOptions special options for this navigation operation
 * @param navigatorExtras extras to pass to the [Navigator]
 *
 * @throws IllegalArgumentException if the given route is invalid
 */
fun <T> NavController.navigateForResult(
    route: String,
    navResultCallback: NavResultCallback<T>,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null
) {
    setNavResultCallback(navResultCallback)
    navigate(route, navOptions, navigatorExtras)
}

/**
 * Navigate to a route in the current NavGraph. If an invalid route is given, an
 * [IllegalArgumentException] will be thrown.
 *
 * @param route route for the destination
 * @param navResultCallback the navigation result callback
 * @param builder DSL for constructing a new [NavOptions]
 *
 * @throws IllegalArgumentException if the given route is invalid
 */
@Suppress("unused", "unused", "unused")
fun <T> NavController.navigateForResult(
    route: String,
    navResultCallback: NavResultCallback<T>,
    builder: NavOptionsBuilder.() -> Unit
) {
    setNavResultCallback(navResultCallback)
    navigate(route, builder)
}

And Before calling

 navController.navigate(route= "${MainDestinations.xxx}/${id}")

call

navController.setNavResultCallback<Unit>(callback= some callback function here)

Then before calling

 navController.navigateUp()

call

navController.getNavResultCallback<Unit>()?.invoke(Unit)
Trueblue answered 29/8, 2023 at 18:2 Comment(0)
S
0

It would help to share more context about how your project is structured, as there might be more elegant options, but you have a few options that should work regardless.

It appears you are using a Fragment or an Activity, so you could either of the following:

  1. Register the ViewModel as a lifecycle observer of the Fragment or Activity and do a new fetch when onStart is called
  2. Make the Fragment or Activity call fetchData in the onStart callback

For both options, you would remove the "LaunchedEffect" block from the Composable

Southeastwards answered 3/6, 2023 at 16:59 Comment(1)
This would not work as I am only navigating between Composables not between Fragments or Activities.Rarely
K
0

You need to 'subscribe' to the 'onStart' lifecycle event, something like this:

val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // ViewModel is of type BaseViewModel
        viewModel.onStart()
    }
}

In BaseViewModel.kt

open fun onStart() {}

The onStart() method will be called every time the screen is shown (when first entering the screen and when popping the backstack back)

Knighterrant answered 15/9, 2024 at 17:0 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.