Hey recently I had discovered this weird behaviour after updating gradle to use navigation-compose (for leveraging new APIs' for compose navigations) navigation-ui(old navigation using graphs)
Problem
We have an auto-logout mechanism where after specific interval we kill activity hosting the fragments and launch Logout activity (which redirects to login activity again)
Setup 1 Scinario
Using navigation-fragment and navigation-ui 2.3.5
(per compose release)
If we are on any fragment within app and timeout happens we kill the activity which destroys the activity and all fragments and launches logout activity. No issues found.
Setup 2 Scinario
Recently we updated to navigation-fragment and navigation-ui 2.7.7
(post compose release) and added navigation-compose:2.7.7 along with the same.
Here is what build.gradle looks like
//Android Navigation Architecture
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation ("androidx.navigation:navigation-ui-ktx:2.7.7")
implementation "androidx.navigation:navigation-compose:2.7.7"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
Update - Removing androidx.navigation:navigation-compose and androidx.lifecycle:lifecycle-runtime-compose produces same effect. So looks like issue is happening after update from 2.3.5 -> 2.7.7.
Now if we destroy activity on timeout we get an exception
NOTE : The issue only occurs if we are within the nav tree 2nd onwards node. StartDestination > Fragmet 2(if we kill activity here than only we are seeing the problem). If we kill activity while on start destination we don't see the issue.
FATAL EXCEPTION: main
Process: appId.uat, PID: 2152
java.lang.RuntimeException: Unable to destroy activity {appId.uat/*.activity.DashBoardActivity}: java.lang.IllegalStateException: no event down from INITIALIZED in component NavBackStackEntry(1dfeaea1-7bd0-413f-9495-63e344e82742) destination=Destination(appId.uat:id/fragmentWeAreOn) label=FragmentWeAreOn class=*.FragmentWeAreOn
at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5613)
at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5645)
at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:47)
at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:98)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8177)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.IllegalStateException: no event down from INITIALIZED in component NavBackStackEntry(1dfeaea1-7bd0-413f-9495-63e344e82742) destination=Destination(appId.uat:id/fragmentWeAreOn) label=FragmentWeAreOn class=*.FragmentWeAreOn
at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.kt:126)
at androidx.lifecycle.LifecycleRegistry.setCurrentState(LifecycleRegistry.kt:106)
at androidx.navigation.NavBackStackEntry.updateState(NavBackStackEntry.kt:186)
at androidx.navigation.NavBackStackEntry.handleLifecycleEvent(NavBackStackEntry.kt:166)
at androidx.navigation.NavController.lifecycleObserver$lambda$2(NavController.kt:190)
at androidx.navigation.NavController.$r8$lambda$bZL_fnLbLD5ZZthGK_6aY8AQ2pA(Unknown Source:0)
at androidx.navigation.NavController$$ExternalSyntheticLambda0.onStateChanged(Unknown Source:2)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.kt:314)
at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.kt:266)
at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.kt:283)
at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.kt:136)
at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.kt:119)
at androidx.fragment.app.Fragment.performDestroy(Fragment.java:3372)
at androidx.fragment.app.FragmentStateManager.destroy(FragmentStateManager.java:812)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:346)
at androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation.complete(SpecialEffectsController.kt:664)
at androidx.fragment.app.SpecialEffectsController$Operation.cancel(SpecialEffectsController.kt:507)
at androidx.fragment.app.SpecialEffectsController.forceCompleteAllOperations(SpecialEffectsController.kt:293)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3037)
at androidx.fragment.app.FragmentManager.dispatchDestroy(FragmentManager.java:2988)
at androidx.fragment.app.FragmentController.dispatchDestroy(FragmentController.java:346)
at androidx.fragment.app.FragmentActivity.onDestroy(FragmentActivity.java:258)
at androidx.appcompat.app.AppCompatActivity.onDestroy(AppCompatActivity.java:283)
at android.app.Activity.performDestroy(Activity.java:8876)
2024-06-03 19:44:13.821 2152-2152 AndroidRuntime appId.uat E at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1491)
at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5600)
... 13 more
2024-06-03 19:44:18.378 2152-2152 FirebaseCrashlytics appId.uat E Cannot send reports. Timed out while fetching settings.
I tried multiple combination of libs, but nothing seems to work. Can someone please help if they have faced this issue earlier.
I've checked your sample project. First, I thought the problem was the use of Serializable instead of parcelable, but the problem is the way you are overriding the hashcode:
data class Temp(
val name: String = "Tempo",
val id: String = "Id"
): Serializable {
override fun hashCode(): Int {
return ('a'..'z').random().hashCode()
}
}
On each call to the hashCode function, another random char is generated. You should return a consistent value there.
Update:
The root cause probably is that there is a data structure like this one:
public abstract class NavigatorState {
private val _transitionsInProgress:MutableStateFlow<Set<NavBackStackEntry>>
...
}
using the NavBackStackEntry hashCode:
public class NavBackStackEntry ... {
...
@Suppress("DEPRECATION")
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + destination.hashCode()
immutableArgs?.keySet()?.forEach {
result = 31 * result + immutableArgs.get(it).hashCode()
}
result = 31 * result + lifecycle.hashCode()
result = 31 * result + savedStateRegistry.hashCode()
return result
}
...
}
This is where your args with changing hashCode is affecting the logic. The issue is that the on-destroy accessed NavBackStackEntry hashCode is not the same as the one that has been added to the Set. This NavBackStackEntry hashCode has not been added, so it has not been moved to INITIALIZED yet.
Why this is happening in this version? Maybe some implementation detail has been changed that uses hashCode.
You can read more about hashCode here: Effective Kotlin Item 43: Respect the contract of hashCode