Reputation: 468
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?
Upvotes: 13
Views: 8259
Reputation: 569
I created a reusable composable function LaunchedEffectRunOnce
class LaunchedEffectRunOnceVm(private val state: SavedStateHandle) : ViewModel() {
fun runOnce(lambda: suspend () -> Unit) {
val hasBeenRun = state["hasBeenRun"] ?: false
if (hasBeenRun) {
return
}
state["hasBeenRun"] = true
viewModelScope.launch {
lambda()
}
}
}
@Composable
fun LaunchedEffectRunOnce(lambda: suspend () -> Unit) {
val vm: LaunchedEffectRunOnceVm = viewModel()
LaunchedEffect(Unit) {
vm.runOnce(lambda)
}
}
Usage:
LaunchedEffectRunOnce {
logd("this will only run once for current viewmodel lifetime")
withContext(IO) {
delay(1000)
}
logd("after delay")
}
Upvotes: 1
Reputation: 469
@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
Upvotes: -1
Reputation: 390
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
}
}
}
}
}
}
}}
Upvotes: 0
Reputation: 88232
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.
Upvotes: 17