androidandroid-jetpack-composeandroid-viewmodel

Avoid the same function being triggered repeatedly in the ViewModel when a Jetpack Compose view recomposes during view loading in Android


I'm building an Android app using Jetpack Compose. I have a view that displays user details and uses a ViewModel to manage the data. The user details are fetched from a service or database. However, the fetch call is triggered unnecessarily every time I rotate the screen because of the configuration change. I'd like to prevent this redundant call and optimize the behavior.

NavHost(navController = navController, startDestination = ....) {
   ....
   ....
   composable("userDetails") {
      UserDetailsScreen(userUrl = userUrl)
   }
}
@Composable
fun UserDetailsScreen(
    userUrl: String,
    viewModel: UserDetailsViewModel = hiltViewModel(),
) {
    LaunchedEffect(userUrl) {
       viewModel.fetchUserDetails(userUrl) // <==
       /* How to prevent the above call getting triggered unnecessarily every time 
          I rotate the screen because of the configuration change? */
    }
    val userDetailsState by viewModel.userDetailsState.collectAsState()
}
@HiltViewModel
class UserDetailsViewModel @Inject constructor(private val usersRepository: UsersRepository): ViewModel() {
   .....
   fun fetchUserDetails(url: String) { 
      viewModelScope.launch {
         usersRepository.fetchUserDetails(url).collect { 
            ......
         }
      }
   }
}

Solution

  • To optimize the behavior of your UserDetailsViewModel and prevent unnecessary data fetching on configuration changes like screen rotations in your app, you can leverage the ViewModel's lifecycle. Since ViewModels survive configuration changes, you can check if the data has already been loaded before making another fetch call. Here's how you can adjust your ViewModel and composable function to achieve this:

    ViewModel Changes

    First, you'll want to modify your ViewModel to include logic that checks whether the user details have already been fetched and avoid refetching if that's the case:

    @HiltViewModel
    class UserDetailsViewModel @Inject constructor(private val usersRepository: UsersRepository) : ViewModel() {
        private var isDataLoaded = false
        private var lastUrl: String? = null
    
        val userDetailsState = MutableStateFlow<UserDetails?>(null) // Assuming UserDetails is your data class
    
        fun fetchUserDetails(url: String) {
            // Only fetch data if it hasn't been loaded or if the URL has changed
            if (!isDataLoaded || lastUrl != url) {
                lastUrl = url
                viewModelScope.launch {
                    usersRepository.fetchUserDetails(url).collect { userDetails ->
                        userDetailsState.value = userDetails
                        isDataLoaded = true
                    }
                }
            }
        }
    }
    

    Composable Function Changes

    In your UserDetailsScreen composable, you don't need to make any changes because it correctly initializes the ViewModel and calls fetchUserDetails within a LaunchedEffect, which is tied to the userUrl. The optimization to prevent unnecessary fetches is handled within the ViewModel itself.

    This approach ensures that your app only fetches data when necessary and is more efficient in handling configuration changes like screen rotations.

    EDIT:

    If you would like to use it inside the composable rather than view model, then you should use the rememberSaveable to store the url:

    @Composable
    fun UserDetailsScreen(
        userUrl: String,
        viewModel: UserDetailsViewModel = hiltViewModel(),
    ) {
        // Remember a key or flag indicating whether we've fetched data for this URL
        val fetchedForUrl = rememberSaveable { mutableStateOf("") }
    
        LaunchedEffect(userUrl) {
            // Only fetch if the URL has changed or on first composition
            if (fetchedForUrl.value != userUrl) {
                viewModel.fetchUserDetails(userUrl)
                // Mark as fetched for this URL
                fetchedForUrl.value = userUrl
            }
        }
    
        val userDetailsState by viewModel.userDetailsState.collectAsState()
        // Use userDetailsState to display your UI
    }