Fragment not associated with a fragment manager after screen rotation in Android - android

I have a problem I can't solve. I have two fragments, one called "NotificationListFragment" that can navigate to a second fragment called "InfoNotificationFragment". Before calling the second fragment, I pass two parameters through the Bundle object:
A closure of type "() -> Unit" that needs to be executed when the second fragment is closed;
An object of type "MessageNotificationCompatible".
Both are passed as Parcelable objects. The closure is set inside a class called "TrackedReference", which contains a map of objects of type "WeakReference", in order to avoid implementing marshalling.
Let me show you how this is done in the "NotificationListFragment" class:
adapter.action.observe(viewLifecycleOwner) { action ->
when(action) {
is NotificationAction.SelectionAction -> {
val message = action.infoNotification.toNotificationMessage()
val handler: () -> Unit = {
markInfoNotificationAsReadInRV(message.data.getID())
findNavController().navigateUp()
}
val closeHandler : CloseHandlerCompatible = CloseHandler(handler)
val b = Bundle()
b.putParcelable(InfoNotificationFragment.MESSAGE_KEY, message)
b.putParcelable(InfoNotificationFragment.CLOSURE_KEY, TrackedReference(closeHandler))
findNavController().navigate(R.id.action_notificationListFragment_to_infoNotificationFragment, b)
}
}
}
Inside the "InfoNotificationFragment" class, within the "onCreate()" method, the parameters passed through the Bundle are retrieved:
arguments?.let { bundle->
val closure : TrackedReference<*>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
message = bundle.getParcelable(MESSAGE_KEY, NotificationMessageCompatible::class.java)
closure = bundle.getParcelable(CLOSURE_KEY, TrackedReference::class.java) as TrackedReference<*>
} else {
#Suppress("DEPRECATION")
message = bundle.getParcelable(MESSAGE_KEY)
#Suppress("DEPRECATION")
closure = (bundle.getParcelable(CLOSURE_KEY) as? TrackedReference<*>)!!
}
closeHandler = (closure.get as CloseHandlerCompatible).closeHandler
closure.removeStrongReference()
}
This second fragment has a button that, when clicked, calls the closure that was passed to it as a parameter:
binding.closeButton.setOnClickListener {
// Need for smart cast
val message = message ?: return#setOnClickListener
// I mark the notification it as read in memory
viewModel.markInfoNotificationAsRead(message.data.getID())
// I call the closeHandler method
this.closeHandler()
}
The problem I'm encountering is the following: everything works correctly following the standard flow. However, when the app remains in the background for a long time, and the activity and fragment view are recreated, I can no longer call the "findNavController().navigateUp()" method inside the closure.
Since the problem occurs at an unspecified time, I tried to simulate the same situation by attempting to activate the screen orientation (which is disabled by default for this activity).
Actually, by rotating the screen, I find myself in the same situation as when the app is in the background, so I can simulate this problem. In any case, the exception that I get when I try to call the "findNavController().navigateUp()" method after rotating the screen is the following:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: it.zucchetti.fq.qid, PID: 29563
java.lang.IllegalStateException: Fragment NotificationListFragment{846f563} (fc633911-2609-4220-8236-acfda3b0a01b) not associated with a fragment manager.
at androidx.fragment.app.Fragment.getParentFragmentManager(Fragment.java:1107)
at androidx.navigation.fragment.NavHostFragment$Companion.findNavController(NavHostFragment.kt:375)
at androidx.navigation.fragment.FragmentKt.findNavController(Fragment.kt:29)
at it.zucchetti.qauthenticator.ui.notificationlist.NotificationListFragment$onSuccess$1$handler$1.invoke(NotificationListFragment.kt:158)
at it.zucchetti.qauthenticator.ui.notificationlist.NotificationListFragment$onSuccess$1$handler$1.invoke(NotificationListFragment.kt:155)
at it.zucchetti.qauthenticator.ui.notification.info.InfoNotificationFragment.bindData$lambda$1(InfoNotificationFragment.kt:110)
at it.zucchetti.qauthenticator.ui.notification.info.InfoNotificationFragment.$r8$lambda$Trhe-1LjjI1Cfn283v0VQLwE1u0(Unknown Source:0)
at it.zucchetti.qauthenticator.ui.notification.info.InfoNotificationFragment$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:7792)
at android.widget.TextView.performClick(TextView.java:16112)
at android.view.View.performClickInternal(View.java:7769)
at android.view.View.access$3800(View.java:910)
at android.view.View$PerformClick.run(View.java:30218)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8663)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)
Could you tell me how to solve the problem?
I tried to change the way to go back to the previous fragment, to save the fragment's state, to save the NavHostFragment's state, but I still haven't solved the problem.

Related

Why do Activity intents may need setExtrasClassLoader but there is nothing similar for Fragment arguments?

I am having a problem with error: Class not found when unmarshalling: com.package.MyParcelableClass (which somehow only presents on release build)
I read about using setExtrasClassLoader to make it clear that the parcelable extra should be parsed with that class.
Intent(from, MyActivity::class.java)
.apply {
setExtrasClassLoader(MyParcelableClass::class.java.classLoader)
putExtra("SOME_PARAM", param)
}
or:
#onCreate
setExtrasClassLoader(MyParcelableClass::class.java.classLoader)
val myParam: MyParcelableClass? = intent.getParcelableExtra("SOME PARAM")
That activity creates a fragment to which the same parcelable is sent
fun newInstance(param: MyParcelableClass): MyFragment {
return MyFragment().apply {
arguments = Bundle().apply {
putParcelable("SOME_PARAM", param)
}
}
}
but there is nothing similar to tell the parcelable which class to use. Why is the design different? Specially because my app is only failing when the fragment is added, the activity seems to be doing fine without specifying the class loader.
I also read that using the same intent param for both activity intent and fragment argument may cause issue, but I see no logic in that, it has never happened for me before.

How to use registerForActivityResult with androidx.fragment.app.Fragment?

ActivityResultLauncher<Intent> launcherImportFileSelection = requireActivity().registerForActivityResult(...
If the above code is placed in onCreate(Bundle savedInstanceState), the following exception is thrown:
Exception: LifecycleOwner MyActivity#868498a is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
If it is placed in the constructor or as a declaration, requireActivity() throws the following exception:
java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
2021-04-17 15:15:41.948 27930-27930/net.biyee.onvifer E/AndroidRuntime: at androidx.fragment.app.Fragment.instantiate(Fragment.java:613)
... 42 more
Caused by: java.lang.IllegalStateException: Fragment MyFragment{fba6d22} (e0d1f006-996d-4051-9839-4575a92e33dd) not attached to an activity.
at androidx.fragment.app.Fragment.requireActivity(Fragment.java:928)
I have the following in build.gradle:
implementation 'androidx.fragment:fragment:1.3.2'
Could anyone offer a tip on this?
Update:
The problem has been solved. Please see my exchange with #CommmonsWare.
You need to set a register call outside the onCreate() method and in addition you need to put the registerForActivityResult() variable as a property of the fragment class. (Works only for Activity instead of Fragment!)
Register call example for KOTLIN:
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
Register call example for JAVA:
// GetContent creates an ActivityResultLauncher<String> to allow you to pass
// in the mime type you'd like to allow the user to select
ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(),
new ActivityResultCallback<Uri>() {
#Override
public void onActivityResult(Uri uri) {
// Handle the returned Uri
}
});
This documentation will help you to understand and practice. Cheers :)

Android Kotlin: Fragment not attached to a context

I am trying to use a TabLayout with different fragments and have started with AndroidStudio's automatically generated code for the tabbed layout. I have not changed how the placeholder fragment is created, displayed, handled etc.: The fragment is handled by a FragmentPagerAdapter, which is used by a ViewPaper, which in turn is used to setup the TabLayout.
The layout already included a FAB. Its onClick looks like this:
fab.setOnClickListener { view ->
val currentFragment: Fragment = sectionsPagerAdapter.getItem(viewPager.currentItem)
when (viewPager.currentItem) {
0 -> doSomething()
1 -> (currentFragment as PlaceholderFragment).fabOnClick()
else -> doSomethingElse()
}
}
Eventhough the above code makes sure that fabOnClick() is only called on the currently visible fragment, when I am trying to get a context using requireContext() in the PlaceholderFragment, java throws the following exception:
java.lang.IllegalStateException: Fragment PlaceholderFragment{660c58b} (08f94c5f-64b3-4a50-a1d4-2f3a6c7b491c)} not attached to a context.
For some reason, the context is available in e.g. onResume() in the PlaceholderFragment:
override fun onResume() {
super.onResume()
// Works fine
Toast.makeText(requireContext(), "placeholder", Toast.LENGTH_LONG).show()
}
fun fabOnClick() {
// Throws exception
Toast.makeText(requireContext(), "placeholder", Toast.LENGTH_SHORT).show()
}
I found this thread, Fragment not attached to a context, in which the solution was to commit a fragment transaction but all of this seems to be handled automatically in this case.
I just fixed an error somewhat similar to you in my code. With the same error you are getting.
The idea is that I was initializing a string variable (NOT in MainActivity) with getResources().
However, the variable was not being initialized with context.getResources()
I was able to initialize this variable inside MainActivity and make it static so that I could just copy the value into the variable NOT inside MainActivity.
So, I think you should search for any variables that you are initializing with getResources and see if you are using a context or not.

Fragment already added exception with singleton dialog

I have a progress dialog which is a singleton because I want it to be shown only once if it gets called multiple times, and doesn't stack on itself. but somewhere in my code, it gets called simultaneously from two places and I get fragment already added exception.
I'm checking if the dialog is not added to activity then I call Dialog.show() but since the method gets called simultaneously from two places, before the first one is added to activity the other one is passed throw if statement and that causes the problem.
I want two thread-safe the function so that multiple threads cant call it simultaneously.
as you can see I've tried synchronizing it by #Synchronized annotation but it didn't work
class ProgressDialogFragment private constructor() : DialogFragment() {
companion object {
private var dialogInstance: DialogFragment? = null
#JvmStatic
#Synchronized
fun showDialog(fragmentManager: FragmentManager) {
if (dialogInstance == null) {
dialogInstance = ProgressDialogFragment().apply {
setStyle(STYLE_NORMAL, R.style.Dialog_FullScreen)
isCancelable = false
}
}
if (!dialogInstance!!.isAdded && fragmentManager.findFragmentByTag("progress_dialog") == null) {
dialogInstance!!.show(fragmentManager, "progress_dialog")
}
}
as said in the documentation, the DialogFragment#show()
Display the dialog, adding the fragment to the given FragmentManager. This is a convenience for explicitly creating a transaction, adding the fragment to it with the given tag, and committing it.
FragmentTransactions commits are async, to make sure it is committed before you move on just call executePendingTransactions() :
if (!dialogInstance!!.isAdded && fragmentManager.findFragmentByTag("progress_dialog") == null) {
dialogInstance!!.show(fragmentManager, "progress_dialog")
fragmentManager.executePendingTransactions()
}
As for the #Synchronized it's unnecessary in this situation because this function can only run in the UI Thread.

Android LiveData, ViewModel, Cannot add the same observer with different lifecycles

I'm new in android architecture components, and trying to use LiveData in my activity and MyLifecycleService, but sometimes the app crashed with
IllegalArgumentException: Cannot add the same observer with different lifecycles
here is my code in service
private final MutableLiveData<SocketStatus> socketStatusMutableLiveData = OrderRxRepository.Companion.getInstance().getMldSocketStatus();
socketStatusMutableLiveData.observe(this, socketStatus -> {
if (socketStatus == null) return;
...
});
for my activity I have activityViewModel class which contains the same livedata, here is the code
class MyActivityViewModel: ViewModel() {
val socketStatusMutableLiveData = OrderRxRepository.instance.mldSocketStatus
}
and the code in my activity
MyActivityViewModel viewModel = ViewModelProviders.of(this).get(MyActivityViewModel .class);
viewModel.getSocketStatusMutableLiveData().observe(this, socketStatus -> {
if (socketStatus == null) return;
...
});
tl;dr You can't call LiveData.observe() with two different LifecycleOwners. In your case, your Activity is one LifecycleOwner and the other is your Service.
From Android's source code you can see that this exception is thrown if there is already a LifecyclerOwner observing and that LifecyclerOwner is different from the one you are trying to observe with.
public void observe(#NonNull LifecycleOwner owner, #NonNull Observer<T> observer) {
...
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
...
}
This explains why you are having this problem since you are trying to observe on the same LiveData with an Activity (which is one LifecycleOwner) and a Service (a different LifecycleOwner).
The bigger problem is that you are trying to use LiveData for something it wasn't meant to do. LiveData is meant to hold data for a single LifecycleOwner while you are trying to make it hold data for multiple LifecycleOwner.
You should consider other solutions to the problem you tried to solve with LiveData. Here are some alternatives depending on your needs:
Global singleton - great if you want to keep some data in memory and have it accessible everywhere in your app. Use it with Rx if you want your data to be "observable"
LocalBroadcastManager - great if you want to communicate between your service and activity
Intent - great if you want to also make sure your activity is alive once your service completes

Categories

Resources