9

I want to run the code only once when the composable is loaded. So I am using LaunchedEffect with key as true to achieve this.

LaunchedEffect(true) {
    // do API call
}

This code is working fine but whenever there is any configuration change like screen rotation this code is executed again. How can I prevent it from running again in case of configuration change?

Abhinav
  • 429
  • 5
  • 13
  • Use viewmodel ,it's one of the jetpack compose library .from viewmodel you can execute network request ,viewmodel will handle all configuration changes – d-feverx Oct 19 '21 at 10:54
  • Does this answer your question? [Jetpack Compose saving state on orientation change](https://stackoverflow.com/questions/63733944/jetpack-compose-saving-state-on-orientation-change) – Sergei S Oct 19 '21 at 11:02

3 Answers3

12

The simplest solution is to store information about whether you made an API call with rememberSaveable: it will live when the configuration changes.

var initialApiCalled by rememberSaveable { mutableStateOf(false) }
if (!initialApiCalled) {
    LaunchedEffect(Unit) {
        // do API call
        initialApiCalled = false
    }
}

The disadvantage of this solution is that if the configuration changes before the API call is completed, the LaunchedEffect coroutine will be cancelled, as will your API call.

The cleanest solution is to use a view model, and execute the API call inside init:

class ScreenViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // do API call
        }
    }
}

@Composable
fun Screen(viewModel: ScreenViewModel = viewModel()) {
    
}

Passing view model like this, as a parameter, is recommended by official documentation. In the prod code you don't need to pass any parameter to this view, just call it like Screen(): the view model will be created by default viewModel() parameter. It is moved to the parameter for test/preview capability as shown in this answer.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Passing viewmodels to composables that render to the UI is bad. It makes your composable less reusable. Hoist the viewmodel to a composable that is used to manage state. – Johann Oct 19 '21 at 12:55
  • 1
    @AndroidDev this way is recommended by [official documentation](https://developer.android.com/jetpack/compose/state#viewmodels-source-of-truth). In the prod code you don't need to pass any parameter to this view, the view model will be created by calling `viewModel()`. It is moved to the parameter for test/preview capability as shown in [this answer](https://stackoverflow.com/a/69089891/3585796). – Phil Dukhov Oct 19 '21 at 13:25
  • 1
    Google clearly added that recently. And it's a really bad idea. I sure as hell won't be using it. As already stated, passing a viewmodel makes your composable far less reusable. I'll stick to hoisting it another composable. Don't follow or trust everything Google posts. They have a lot of novice developers there who lack a lot of experience. – Johann Oct 19 '21 at 13:38
  • @AndroidDev Any tool can ruin your app if you don't know how to use it. View models give you a lot of flexibility, and no one is stopping you from splitting the screen into smaller views, each without a view model as a dependency and can be easily tested. – Phil Dukhov Oct 19 '21 at 13:44
  • Google needs to clean up their docs. On the same page that you linked to, under the section "State hoisting", it clearly outlines all the benefits that go completely contrary to passing in a viewmodel. But hey, many devs will pick the quick and dirty route. – Johann Oct 19 '21 at 13:51
  • 1
    @AndroidDev If you think you can improve the documentation, you can [create an issue](https://issuetracker.google.com/issues/new?component=192697&template=845603) with your suggestions. – Phil Dukhov Oct 19 '21 at 13:59
  • If I am supposed do the API call inside the init of the ViewModel, how do I pass it a value from a navigation argument. – AndroidKotlinNoob Apr 25 '22 at 08:07
  • 1
    @AndroidKotlinNoob You can add `SavedStateHandle` parameter to your view model, check out [this answer](https://stackoverflow.com/a/69145748/3585796) – Phil Dukhov Apr 25 '22 at 08:11
  • The downside with `ViewModel` approach is that you can't write unit test for the codes in `init` block. – Mohammad Sianaki Nov 22 '22 at 09:47
  • @MohammadSianaki of course you can. You can just create the tested class as part of your test. – Michał Klimczak Dec 13 '22 at 10:34
  • 1
    @Johann please do Compose tutorial first – user924 Apr 14 '23 at 11:00
1

I assume the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the loadState is called only one time, and also guarantee the service itself is not called unless someone is listening to it. Then you listen to the state from the viewmodel, and no need to call anything api call from launched effect, also your code will be reacting to specic state.

Here is a code example

class MyViewModel : ViewModel() {
private val uiScreenState: : MutableStateFlow<WhatEverState> =
    MutableStateFlow(WhatEverIntialState).also {
        loadState()
    }

fun loadState(): StateFlow<WhatEverState>> {
    return users
}

private fun loadUsers() {
    // Do an asynchronous operation to fetch users.
}
}

When using this code, you do not have to call loadstate at all in the activity, you just listen to the observer.

You may check the below code for the listening

class MyFragment : Fragment {
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    return ComposeView(requireContext()).apply {
        setContent {
            StartingComposeTheme {
                Box(modifier = Modifier.fillMaxSize()) {
                    val state by viewModel.uiScreenState.collectAsState()
                    when (state) {
                        //do something
                    }
                }
            }
        }
    }
}

}}

Islam Mansour
  • 372
  • 2
  • 15
0

@Islam Mansour answer work good for dedicated viewModel to UI but my case is shared ViewModel by many UIs fragments

In my case above answers does not solve my problem for calling API for just only first time call when user navigate to the concerned UI section.

Because I have multiple composable UIs in NavHost as Fragment

And my ViewModel through all fragments

so, the API should only call when user navigate to the desired fragment

so, the below lazy property initialiser solve my problem;

val myDataList by lazy {
    Log.d("test","call only once when called from UI used inside)")
    loadDatatoThisList()
    mutableStateListOf<MyModel>()
}

mutableStateListOf<LIST_TYPE> automatically recompose UI when data added to this

variable appeded by by lazy intialized only once when explicilty called

Mir
  • 411
  • 1
  • 7
  • 16
  • Having sharedViewModel I believe does not prevent you from using another view model for your fragment specific needs which you still can initiate your stuff using the below solution. – Islam Mansour Mar 07 '22 at 09:01