Why LaunchedEffect call second time when I navigate back? - android

I want to show some data and simulate loading. For this reason I use LaunchedEffect on the first screen. It works fine, but when I add navigation, LaunchedEffect launch twice.
Navigation: First (LaunchedEffect) -> Second -> Back to first (LaunchedEffect launch again)
I expect that when I return to the first screen LaunchedEffect won't launch and I will immediately see the data.
Sample of LaunchedEffect:
#Composable
fun FirstScreen(...) {
...
LaunchedEffect(Unit) {
state = State.Loading
delay(2_000L)
state = State.Success(...)
}
}
Sample of navigation:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = FIRST_ROTE
) {
composable(FIRST_ROTE) { FirstScreen(...) }
composable(SECOND_ROTE) { SecondScreen(...) }
}

See what the documentation says
To call suspend functions safely from inside a composable, use the
LaunchedEffect composable. When LaunchedEffect enters the Composition,
it launches a coroutine with the block of code passed as a parameter.
The coroutine will be cancelled if LaunchedEffect leaves the
composition. If LaunchedEffect is recomposed with different keys, the existing coroutine will be
cancelled and the new suspend function will be launched in a new
coroutine.
When you navigate to another screen, the composable is removed from the composition (which means it is not drawn). This is why the LaunchedEffect is executed again.
You can control this execution using the state in view model. Something like:
#Composable
fun FirstScreen(vm: YourViewModel) {
LaunchedEffect(Unit) {
if (vm.someFlag == true) {
vm.someFlag = false
state = State.Loading
delay(2_000L)
state = State.Success(...)
}
}
}

Related

How to add a Listener and Receive a callback whenever the navigation transition has finished in Jetpack Compose (NavHostController)?

I am trying to add a Listener and receive a callback whenever the navigation transition has finished in Jetpack Compose.
I have tried to use the NavController API addOnDestinationChangedListener but it is send immediately to my listener and is not waiting for the composition to finish.
val navController = rememberNavController()
// Register the destination changed listener
navController.addOnDestinationChangedListener { _, destination, _ ->
// destination change is sent immediately and isnt waiting for the composable to finish
}
My goal is to add a listener that is only fired once the composition is completed and the destination is changed.
something like this:
// Register the transition finished listener
navController.transitionFinished{ _, destination ->
// Do something when the navigation transition has finished
}
NavHost(navController = navController, startDestination = "Home") {
composable("Home") {
Text("FIRST SITE")
//FIRE NOW THE CALLBACK AFTER IT FINISHED COMPOSITION
}
composable("Settings") {
Text("SECOND SITE")
//FIRE NOW THE CALLBACK AFTER IT FINISHED COMPOSITION
}
}
Where it will only fire callback once the whole composable is finished its composition.
Are there options to get the current tranistioning state of the navHost so I can implement it myself or any other API calls I can use?
I'm a bit confused by your code because it seems you are mixing classic AndroidX Navigation (findNavController()) and Navigation Compose here.
Here's a full Compose example of what you want to achieve:
val navController = rememberNavController()
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute by remember { derivedStateOf { currentBackStackEntry?.destination?.route ?: "Home" } }
NavHost(
navController = navController,
startDestination = "Home",
) {
...
}
The value of currentRoute contains the current route of the NavController and since it's a State it automatically updates when the destination changes.

Navigation is being called every time in Jetpack Compose

I'm implementing registration in my application and, after filling in the respective fields, I click on a button that will make a registration request to the API. Meanwhile, I place a Loading View and when I receive the successful response, I execute the navigation to the OnBoarding screen. The issue is that the navController is always running the navigation and doing the navigation and popUp several times, when it should only do it once. I always get this warning on logs: Ignoring popBackStack to destination 29021787 as it was not found on the current back stack and I am not able to do any click or focus in the OnBoardingScreen.
My code:
val uiState by registerViewModel.uiState.collectAsState()
when (uiState) {
is BaseViewState.Data -> {
navController.navigate(NavigationItem.OnBoarding.route) {
popUpTo(NavigationItem.Register.route) {
inclusive = true
}
}
}
is BaseViewState.Loading -> LoadingView()
is BaseViewState.Error -> BannerView()
else -> {}
}
On button click I call the viewModel like this:
registerViewModel.onTriggerEvent(
RegisterEvent.CreateUser(
usernameInputState.value.text,
emailInputState.value.text,
passwordInputState.value.text
)
)
And, in ViewModel, I do my request like this:
override fun onTriggerEvent(eventType: RegisterEvent) {
when (eventType) {
is RegisterEvent.CreateUser -> createUser(eventType.username, eventType.email, eventType.password)
}
}
private fun createUser(username: String, email: String, password: String) = safeLaunch {
setState(BaseViewState.Loading)
execute(createUser(CreateUser.Params(username, email, password))) {
setState(BaseViewState.Data(RegisterViewState(it)))
}
}
I guess it should be caused by recomposition, because I put a breakpoint on first when scenario and it stops here multiple times, but only one on ViewModel. How can I fix this?
This issue is here
is BaseViewState.Data -> {
navController.navigate(NavigationItem.OnBoarding.route) {
popUpTo(NavigationItem.Register.route) {
inclusive = true
}
}
}
Every time you call navController.navigate NavHost will keep on passing through this block, executing an endless loop.
I suggest having the navigate call from a LaunchedEffect with a key (like this),
LaunchedEffect(key1 = "some key") {
navController.navigate(…)
}
or creating a separate structure namely "Events" where they are emitted as SharedFlow and observed via a Unit keyed LaunchedEffect
LaunchedEffect(Unit) {
viewModel.event.collectLatest {
when (it) {
is UiEvent.Navigate -> {
navController.navigate(…)
}
}
}
}

When flow collect stop itself?

There is ParentFragment that shows DialogFragment. I collect a dialog result through SharedFlow. When result received, dialog dismissed. Should I stop collect by additional code? What happens when dialog closed, but fragment still resumed?
// ParentFragment
private fun save() {
val dialog = ContinueDialogFragment(R.string.dialog_is_save_task)
dialog.show(parentFragmentManager, "is_save_dialog")
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
}
class ContinueDialogFragment(
#StringRes private val titleStringId: Int,
#StringRes private val messageStringId: Int? = null
) : DialogFragment() {
private val _resultSharedFlow = MutableSharedFlow<Int>(1)
val resultSharedFlow = _resultSharedFlow.asSharedFlow()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { context ->
AlertDialog.Builder(context)
.setTitle(getString(titleStringId))
.setMessage(messageStringId?.let { getString(it) })
.setPositiveButton(getString(R.string.dialog_yes)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_YES)
}
.setNegativeButton(getString(R.string.dialog_no)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_NO)
}
.setNeutralButton(getString(R.string.dialog_continue)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_CONTINUE)
}
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
companion object {
const val RESULT_YES = 1
const val RESULT_NO = 0
const val RESULT_CONTINUE = 2
}
}
When a Flow completes depends on its original source. A Flow built with flowOf or asFlow() ends once it reaches the last item in its list. A Flow built with the flow builder could be finite or infinite, depending on whether it has an infinite loop in it.
A flow created with MutableSharedFlow is always infinite. It stays open until the coroutine collecting it is cancelled. Therefore, you are leaking the dialog fragment with your current code because you are hanging onto its MutableSharedFlow reference, which is capturing the dialog fragment reference. You need to manually cancel your coroutine or collection.
Or more simply, you could use first() instead of collect { }.
Side note, this is a highly unusual uses of a Flow, which is why you're running into this fragile condition in the first place. A Flow is for a series of emitted objects, not for a single object.
It is also very fragile that you're collecting this flow is a function called save(), but you don't appear to be doing anything in save() to store the instance state such that if the activity/fragment is recreated you'll start collecting from the flow again. So, if the screen rotates, the dialog will reappear, the user could click the positive button, and nothing will be saved. It will silently fail.
DialogFragments are pretty clumsy to work with in my opinion. Anyway, I would take the easiest route and directly put your behaviors in the DialogFragment code instead of trying to react to the result back in your parent fragment. But if you don't want to do that, you need to go through the pain of calling back through to the parent fragment. Alternatively, you could use a shared ViewModel between these two fragments that will handle the dialog results.
I believe you will have a memory leak of DialogFragment: ParentFragment will be referencing the field dialog.resultSharedFlow until the corresponding coroutine finishes execution. The latter may never happen while ParentFragment is open because dialog.resultSharedFlow is an infinite Flow. You can call cancel() to finish the coroutine execution and make dialog eligible for garbage collection:
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}

conditional navigation in compose, without click

I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}

Is there a way to make a navigation work inside a handler using Jetpack Compose?

I am trying to create a splash screen using Jetpack Compose. I created my navigation and I have all my IDs to go to different screens, but I cannot make a screen navigate to another inside of a Hadler. How do you guys go about that?
#Composable
fun GoToMainScreen(navController: NavHostController){
Handler(Looper.getMainLooper()).postDelayed(object : Thread() {
override fun run() {
navController.navigate("main_screen")
Log.i("LOOPER", "It got here!")
}
}, 4000L)
}
I've tried this, and in my case it navigates fine with your composable:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "profile") {
composable("profile") {
GoToMainScreen(navController)
}
composable("main_screen") { Text("main_screen") }
}
Not sure what's different in your case, but in compose we wouldn't usually need Handler
First of all, you need to wrap creation of the handler LaunchedEffect, otherwise your handler may be created many times in case of screen recomposition.
And inside LaunchedEffect we can use coroutine, so same with much less code looks like:
#Composable
fun GoToMainScreen(navController: NavHostController) {
LaunchedEffect(Unit) {
delay(2000L)
navController.navigate("main_screen")
}
}
If this still doesn't help make sure providing a minimal-reproducible-example, something like my first block of code.

Categories

Resources