Restore Fragment within a Fragment Pager Adapter - android

I have a Fragment Pager Adapter with 5 fragments with a RecyclerView, all the same but the last one, which has a button to alter the contents, when I press this button it opens a Dialog Fragment that will replace all the contents of the RecyclerView.
My problem comes only when I send the app to background while the Dialog Fragment is open, when I resume the app and submit the changes, the app crashes showing that the RecyclerView is null.
How can I recover the full state of the Fragment after resuming.
By Logging the Life cycle I can see that after resume OnCreate, OnCreateView & OnActivityCreated are run, but my method setUpCustomPatternsRecyclerView reports that RecyclerView is null.
Below is the code for my Fragment(Very simple)
IMPORTANT: The App crashes only when I try it in my phone, in 4 different emulators, the App DO NOT crashes.
Is it the the UI is not fully drawn in my phone?
If so how can I get notified that it is fully drawn?
Fragment:
public static FragmentCustomPatterns newInstance(ArrayList<PatternArrays> patternArrays, String fragmentTitle, int patternSelectedMode) {
FragmentCustomPatterns patterns = new FragmentCustomPatterns();
Bundle args = new Bundle();
args.putParcelableArrayList(CUSTOM_PATTERNS, patternArrays);
args.putInt(PATTERN_SELECTED_MODE, patternSelectedMode);
args.putString(TITLE, fragmentTitle);
patterns.setArguments(args);
return patterns;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
patternSelectedMode = getArguments().getInt(PATTERN_SELECTED_MODE);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
openCustomCoordinatesFragment = (OpenCustomCoordinatesFragment) getActivity();
View view = inflater.inflate(R.layout.fragment_custom_patterns, container, false);
FloatingActionButton floatingActionButton = view.findViewById(R.id.fab_custom_patterns);
floatingActionButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
openCustomCoordinatesFragment.openCoordinatesDialog();
}
});
recyclerFlag = false;
return view;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
activityPatternSelector = (ActivityPatternSelector) getActivity();
setUpCustomPatternsRecyclerView();
}
public void setUpCustomPatternsRecyclerView() {
recyclerPatternsView = getView().findViewById(R.id.custom_pattern_recycler_view);
adapter = new PatternCoordinatesAdapter(getActivity(),activityPatternSelector.getCustomPatternArrays(), patternSelectedMode);
recyclerPatternsView.setAdapter(adapter);
Utilities utilities = new Utilities(getActivity(), R.layout.item_coordinates_pattern_cardview);
GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), utilities.calculateNoOfColumns());
recyclerPatternsView.setLayoutManager(gridLayoutManager);
recyclerPatternsView.setItemAnimator(new DefaultItemAnimator());
if (!recyclerFlag) {
recyclerPatternsView.addItemDecoration(new GridSpacing(utilities.calculateSpacing()));
recyclerFlag = true;
}
}
public interface OpenCustomCoordinatesFragment {
void openCoordinatesDialog();
}
Emulator(API22 & API26) logcat of LifeCycle recreating the error:
E/TESTING ACT: onPause:
D/TESTING FRAG: onPause:
E/TESTING ACT: onSaveInstanceState:
D/TESTING FRAG: onSaveInstanceState:
D/TESTING FRAG: onResume:
Phone Logcat(API25) logcat of LifeCycle recreating the error:
E/TESTING ACT: onPause:
D/TESTING FRAG: onPause:
E/TESTING ACT: onSaveInstanceState:
D/TESTING FRAG: onSaveInstanceState:
E/TESTING ACT: onCreate:
D/TESTING FRAG: onCreate:
E/TESTING ACT: onCreate: Array Size 25
D/TESTING FRAG: onCreateView:
D/TESTING FRAG: onActivityCreated:
D/TESTING FRAG: onActivityCreated: Array Size 25
D/TESTING FRAG: onResume:
Both are totally different I'll work my way on the phone logcat to create a Bundle in the OnSaveInstanceState. Note that on the emulator OnResume is called after OnSavedInstanceState while on the phone after OnActivityCreated and none of the other method are called.
Can anybody explain this lifecycle difference between physical devices and emulator?
UPDATE:
I can't find a fix for this. Running in the activity hosting the FragmentPagerAdapter which also launches the DialogFragment:
final FragmentCustomPatterns myTempFrag = (FragmentCustomPatterns) patternsPagerAdapter.getItem(4);
System.out.println("TESTING: Added " + myTempFrag.isAdded() + " Visible " + myTempFrag.isVisible() + " In Layout " + myTempFrag.isInLayout() + " Resumed " + myTempFrag.isResumed() + " Count " + patternsPagerAdapter.getCount());
In normal conditions returns:
TESTING: 'Added' true 'Visible' true 'In Layout' false 'Resumed' true Count 5
After sending the app to background with the Dialog Fragment open, and back to the App again, returns:
TESTING: 'Added' false 'Visible' false 'In Layout' false 'Resumed' false Count 5
I assume that this is the reason that I get:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.widget.RecyclerView.swapAdapter(android.support.v7.widget.RecyclerView$Adapter, boolean)' on a null object reference
Does this means that the fragment is not there?,
How do I interpret this last message?,
How do I fix the 'Added' to true?
PARTIAL FIX:
I have a "Restart Activity" Method, and I'm using it to "fix" my problem, but newbie as I am I don't think that's the proper answer.

Set the RecyclerView on onCreateView, the restoring should go automatically.
About the lifecycle: The difference exists because your emulator didn't destroy your activity / fragments and your device did, probably because the emulator has more memory.
If you would have opened many more apps on the emulator before re-opening your own app, you would have had the same. Or if you, on the emulator, put the developer option "don't keep activities" on, that mimics this behaviour: a device destroying your activities and fragments right away because it wants to use the memory elsewhere.
So any device/emulator can take any of the two routs you see. You should support both.

Related

Re-inflating view in handler.post when getView() is null

I recently replaced all the deprecated AsyncTask code in my apps with handlers and newSingleThreadExecutors. After retrieving response data from a remote server, I update the UI in the handler.post section of the code.
I've never personally been able to reproduce any problems with this, but on some devices (mostly oppo's, redmi's, vivo's, etc) under some real-world conditions, getView() returns null and my stop-gap attempt to re-inflate the view fails. The number of crashes has increased by a lot:
Exception java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.content.Context.getSystemService(java.lang.String)' on a null object reference
Rough outline of my code:
public class ResultFragment extends Fragment {
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.result, container, false);
}
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
Bundle bundle = this.getArguments();
assert bundle != null;
String query_url = bundle.getString("query_url");
send_request(query_url);
}
void send_request(String... urls) {
Handler handler = new Handler(Looper.getMainLooper());
Executors.newSingleThreadExecutor().execute(() -> {
.....
handler.post(() -> {
context = getContext();
final TextView mTextView;
final WebView mWebView;
if (getView() != null) {
mTextView = getView().findViewById(R.id.count);
mWebView = getView().findViewById(R.id.result);
} else {
View view = LayoutInflater.from(context).inflate(R.layout.result, null); <-- crash
mTextView = view.findViewById(R.id.count);
mWebView = view.findViewById(R.id.result);
}
My understanding from the lifecycle documentation is that I should be able to get the view with this code. And I do understand that trying to re-inflate the code like this is a dangerous proposition (crashes might occur!). But how do I do so when getView() returns null?
As I say, I've never been able to replicate these crashes. So I'm open to trying anything that might work.
For general information, I'm targeting sdk version 33.
Your code will crash if the fragment view (or the entire Fragment instance) is destroyed as a result of user leaving your app or screen.
A quick fix for your issue is to cancel the handler runnable execution when the fragment view is destroyed.
#Override
public void onDestroyView() {
super.onDestroyView();
handler.removeCallbacksAndMessages(null);
}
For future development of your app you should look over more advance API to deal with this sort of issue. Ex: RxJava, Kotlin coroutines to name a few.
Small tip that will probably help you to reproduce the crash on any device/emulator - activate the developer option: "Don't keep activity", press the button that makes the network request and then immediately close the application. The thread will post a runnable that will execute after the fragment/view-fragment is destroyed -> NPE crash.
You wouldn't do this at all. If you don't have a view, reinflating it isn't going to do what you expect. It would, at best, create a new set of views that are in memory only and not displayed on the screen. In other words it would be pointless.
Also, that's not what your problem is. You problem is that the context is null. Your fragment isn't attached to any. In this case, what you probably want to do is update any persisted state (if any) and skip updating the UI.
Also, if you're inflating your UI normally on an excutor that then posts to a handler- stop. THat's not how it works. THe inflation should happen in the onCreateView function. You can fill in the views like that, but you would NEVER inflate them like that.

Could not find active fragment with index -1

Activity fragment manager problem When change orientation:
Caused by: java.lang.IllegalStateException: Could not find active fragment with index -1
at
android.support.v4.app.FragmentManagerImpl.restoreAllState(FragmentManager.java:3026)
at
android.support.v4.app.Fragment.restoreChildFragmentState(Fragment.java:1446)
at
android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1380)
at
android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1740)
at
com.motors.mobile.core.v2.DaggerIncludeBaseActivity.onCreate(DaggerIncludeBaseActivity.java:26)
Follow my code :
#Override
protected void tabletPortraitInit(Bundle savedInstanceState) {
super.tabletPortraitInit(savedInstanceState);
openSubFragment();
}
#Override
protected void tableLandscapeInit(Bundle savedInstanceState) {
super.tableLandscapeInit(savedInstanceState);
openSubFragment();
}
protected void openSubFragment() {
Bundle bundle = getIntent().getBundleExtra(CAR_DETAIL_KEY);
fragment = new BuyDetailFragment();
if (getSupportFragmentManager().findFragmentByTag(BuyDetailFragment.TAG) != null)
fragment = (BuyDetailFragment) getSupportFragmentManager().findFragmentByTag(BuyDetailFragment.TAG);
fragment.setArguments(bundle);
menuClickListener = fragment;
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.flMain, fragment, BuyDetailFragment.TAG)
.commit();
// init toolbar items
View tbView = getLayoutInflater().inflate(R.layout.items_detail_menu_layout, toolbar.findViewById(R.id.container), true);
phone = tbView.findViewById(R.id.phone);
message = tbView.findViewById(R.id.message);
link = tbView.findViewById(R.id.notifications);
site = tbView.findViewById(R.id.site);
shortlistView = tbView.findViewById(R.id.wishListMenu);
phone.setOnClickListener((e) -> menuClickListener.clickPhone());
message.setOnClickListener((e) -> menuClickListener.clickMessage());
link.setOnClickListener((e) -> menuClickListener.clickNotifications());
site.setOnClickListener((e) -> menuClickListener.clickSite());
shortlistView.setOnClickListener((e) -> menuClickListener.clickShortlist());
Drawable drawable = ContextCompat.getDrawable(this, R.drawable.vector_heart);
drawable.setColorFilter(ContextCompat.getColor(this, R.color.colorAccent), PorterDuff.Mode.SRC_IN);
shortlistView.changeIcon(drawable);
}
There is no my baseActivity
Does your fragment have setRetainInstance(true)? If so, that may be causing you an issue here, especially if you are using a fragment apart of FragmentStatePagerAdapter.
This can happen with a combination of dismissAllowingStateLoss after onSaveInstanceState and retainInstanceState.
See this helpful example with steps to reproduce (that site does not allow commenting, but it helped me diagnose the issue)
Steps to reproduce:
Open page and show dialog fragment with retainInstance = true
Background app, onSaveInstanceState is called
dismiss dialog in an async task via dismissAllowingStateLoss
perform configuration change, for example by changing language or orientation
open app
crash "Unable to start activity... java.lang.IllegalStateException: Could not find active fragment with index -1"
Under the scenes what's going on is that FragmentManagerImpl.restoreAllState now has an active fragment with an index of -1 because dismissAllowingStateLoss removes the fragment from the backstack, BUT, it is still part of nonConfigFragments because the commit part of dismissAllowingStateLoss was ignored as it was called after onSaveInstanceState.
To fix this will require one of:
not using retainInstanceState on Dialogs that can be dismissed via dismissAllowStateLoss, or
not calling dismiss after state loss
and implementing the desired behavior in a different way.

My Adapter is getting null

I have a viewpager in an activity. I'm loading it like this
ArrayList<Fragment> arrayListFragment = new ArrayList<>();
Fragment first = Fragment.instantiate(MainActivity.this, MainActivityFragment.class.getName());
Fragment second = Fragment.instantiate(MainActivity.this, SecondFragment.class.getName());
arrayListFragment.add(first);
arrayListFragment.add(second);
Log.d(TAG, "Pager created " + second);
mPagerAdapter = new SwipeScreenPagerAdapter(getSupportFragmentManager(),
arrayListFragment, pageTitles); //This is custom adapter
And In onCreateView() of SecondFragment I've this log:
mAdapter = new CustomAdapter(mContext, null, mList);
Log.d(TAG, "Initialised adapter " + this);
Everything works fine when I run the app. The interesting thing happen when I put app in background and then resume it after some time. The activity is restarted and viewpager is initialised again and for some reason Log.d(TAG, "Pager created " + second); and Log.d(TAG, "Initialised adapter " + this); statements print fragment with different ids. They both print same id on the first run but differ when i resume. And mAdapter becomes null when used in a fragment function (because this function is called on fragment received from viewpager).
Log statements are :
Pager created SecondFragment{269ef202}
Initialised adapter SecondFragment{298444fe #2 id=0x7f0e0084 android:switcher:2131624068:1}
From where this new fragment is being created? At first I thought it is somehow restoring the old fragment but I tried passing null in onActivityCreate like this:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(null);
}
but it is still creating separate fragment instance and also it has different id.
I figured out the problem. When the app was resumed, activity was restoring the fragment that was last associated with it in the call
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
so passing null to super.onCreate(null) caused it to avoid restoring last fragment. In my case I needed to create new copy of fragment because my data need to be refetched on every time activity was created so set it null with caution.

Prevent Fragment recovery in Android

We are using Fragments and we don't need them to be automatically recovered when the Activity is recreated.
But Android every time when Activity::onCreate(Bundle savedInstanceState) -> super.onCreate(savedInstanceState) is called, restores Fragments even if we use setRetainInstance(false) for those Fragments.
Moreover, in those Fragments Fragment.performCreateView() is called directly without going through Fragment::onAttach() and so on. Plus, some of the fields are null inside restored Fragment...
Does anybody know how to prevent Android from restoring fragments?
P.S. We know that in case of recreating Activity for config changes it could be done by adding to manifest android:configChanges="orientation|screenSize|screenLayout. But what about recreating activity in case of automatic memory cleaning?
We finished by adding to activity:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
It suppresses any saved data on create/recreate cycle of an Activity and avoids fragments auto re-creation.
#goRGon 's answer was very useful for me, but such use cause serious problems when there is some more information you needs to forward to your activity after recreate.
Here is improved version that only removes "fragments", but keep every other parameters.
ID that is removed from bundle is part of android.support.v4.app.FragmentActivity class as FRAGMENTS_TAG field. It may of course change over time, but it's not expected.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(createBundleNoFragmentRestore(savedInstanceState));
}
/**
* Improve bundle to prevent restoring of fragments.
* #param bundle bundle container
* #return improved bundle with removed "fragments parcelable"
*/
private static Bundle createBundleNoFragmentRestore(Bundle bundle) {
if (bundle != null) {
bundle.remove("android:support:fragments");
}
return bundle;
}
I was having a problem with TransactionTooLargeException. So thankfully after using tolargetool I founded that the fragments (android:support:fragments) were been in memory, and the transaction became too large. So finally I did this, and it worked great.
#Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("android:support:fragments", null);
}
Edit: I added it to the Activity. In my case I have one single Activity app and Multiple Fragments.
Those who got NPE with ViewPager when use this method described in the accepted answer, please override
ViewPager.onRestoreInstanceState(Parcelable state)
method and call
super.onRestoreInstanceState(null);
instead.
I removed the fragments in Activity's onCreate.
For an app with a ViewPager, I remove the fragments in onCreate(), before their creation.
Based on this thread: Remove all fragments from container, we have:
FragmentManager fm = getSupportFragmentManager();
for (Fragment fragment: fm.getFragments()) {
fm.beginTransaction().remove(fragment).commitNow();
}
Use this one for androidx
override fun onCreate(savedInstanceState: Bundle?) {
preventFragmentRecreation()
super.onCreate(savedInstanceState)
}
private fun preventFragmentRecreation() {
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
savedStateRegistry.unregisterSavedStateProvider("android:support:fragments")
}
}
This worked for me
#Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
}
View hierarchy in not restored automatically. So, in Fragment.onCreateView() or Activity.onCreate(), you have to restore all views (from xml or programmatically). Each ViewGroup that contains a fragment, must have the same ID as when you created it the first time. Once the view hierarchy is created, Android restores all fragments and put theirs views in the right ViewGroup thanks to the ID. Let say that Android remembers the ID of the ViewGroup on which a fragment was. This happens somewhere between onCreateView() and onStart().

onSaveInstanceState() and onRestoreInstanceState(Parcelable state) are not called?

I have a custom view that extends LinearLayout. I have implemented onSaveInstanceState() and onRestoreInstanceState() to save the current view state. However no any action is taken. When I place a log inside of those two methods also nothing appears in Log Cat. I assume that those two methods are not even called. Can anybody explain where is the problem? Thanks.
#Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putInt("currentPage", currentPage);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
currentPage = bundle.getInt("currentPage");
Log.d("State", currentPage + "");
super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
return;
}
super.onRestoreInstanceState(state);
}
After digging in android os I have finally figured it out. As I suspected: there is nothing wrong with those two methods. They are just not called.
On the Web you can read that 'onRestoreInsatnceState is called when activity is re-created' Ok, it make sens but it's not completely true. Yes, onRestoreInstanceState() is called when activity is recreated but only iff:
it was killed by the OS. "Such situation happen when:
orientation of the device changes (your activity is destroyed and recreated)
there is another activity in front of yours and at some point the OS kills your activity in order to free memory (for example). Next time when you start your activity onRestoreInstanceState() will be called."
So if you are in your activity and you hit Back button on the device, your activity is finish()ed and next time you start your app it is started again (it sounds like re-created, isn't?) but this time without saved state because you intentionally exited it when you hit Back button.
As Steven Byle's comment mentioned, a custom View must have an id assigned to it for onSaveInstanceState to be called. I accomplished this by setting an id in my custom View constructor:
public class BoxDrawingView extends View {
private int BOX_DRAWING_ID = 555;
…
public BoxDrawingView(Context context, AttributeSet attrs) {
…
this.setId(BOX_DRAWING_ID);
}
…
}

Categories

Resources