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.
Related
When my activity containing viewpager is killed by system in background and then restores its state, fragments are correctly created and viewpager adapter can also point to them correctly.
But when I get a fragment reference and try to access its fields, they are all null (checked by using breakpoint).
I checked this by placing breakpoints in fragment onCreateView() and in my activity button's clickListener.
((WelcomeFragment)homeActivityFragmentPageAdapter.getItem(POSITION_HOME)).setdata(myData);
Now this method will through null pointer exception since setdata(data) is internally accessing arraylist field of fragment.
This creates a problem for me since, my activity has to continuously feed network data to the fragment by calling its public method (as suggested by documentation).
How to insure that after state restored; correct instance is pointed in my activity.
Try to use instantiateItem adapter method instead getItem.
((WelcomeFragment)homeActivityFragmentPageAdapter.instantiateItem(mViewPager, POSITION_HOME)).setdata(myData);
Method getItem is overrided method, and common use is creation of child fragments.
EDIT:
In case of the question's scenario, you also need to store the state of FragmentStatePagerAdapter manually:
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState
.putParcelable("pages",homeActivityFragmentPageAdapter.saveState());
super.onSaveInstanceState(savedInstanceState);
}
Then you can retrieve the state in oncreate:
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
homeActivityFragmentPageAdapter.restoreState(savedInstanceState.getParcelable("pages"),this.getClassLoader());
welcomeFragment = (WelcomeFragment) homeActivityFragmentPageAdapter.instantiateItem(mViewPager, POSITION_HOME);
}
else { //simply create a new instance here}
homeActivityFragmentPageAdapter.addFragmentToAdapter(welcomeFragment);
homeActivityFragmentPageAdapter.notifyDataSetChanged();
}
I am using appcompat support library v22 and creating a ViewPager from MainActivity with three fragments. Following is the code to setup ViewPager which is called from onCreate() of MainActivity.
private void setupViewPager(ViewPager viewPager) {
mTabAdapter = new Adapter(getSupportFragmentManager());
mTabAdapter.addFragment(new CarFragment(), "Cars");
mTabAdapter.addFragment(new VehiclesRecycleListFragment(), "Vehicles");
mTabAdapter.addFragment(new BikeFragment(), "Bikes");
viewPager.addOnPageChangeListener(new InternalViewPagerListener());
viewPager.setAdapter(mTabAdapter);
}
All three fragments use LoaderManager to load data and showing it in RecyclerView. The issue is that one of the fragment (VehiclesRecycleListFragment), throws "IllegalStateException: Fragment not attached to Activity" in getLoaderManager() randomly after running for a long time (maybe after a few resumes). Since we are catching that exception, the fragment is not crashing but the functionalities are crippled now. Before this point, application is running fine.
We tried to debug into getLoaderManager, and following is the code which throws exception after checking mHost
/**
* Return the LoaderManager for this fragment, creating it if needed.
*/
public LoaderManager getLoaderManager() {
if (mLoaderManager != null) {
return mLoaderManager;
}
if (mHost == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mCheckedForLoaderManager = true;
mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, true);
return mLoaderManager;
}
I am not able to understand is that if a fragment is displaying and running fine inside ViewPager, how come it's not attached to activity.
Please help me to resolve this, let me know if you need more details.
Given a fragment which loads (a lot of) data from the database using a loader.
Problem :
I have a pager adapter which destroys the fragment when the user moves away from the tab holding it and recreates it when user gets back to that tab. Because of this recreation, a new loader is created everytime and the data gets loaded everytime.
Question :
To avoid recreating loader everytime the fragment is created, is it ok to use getActivity.getSupportLoaderManager.initLoader(loaderId, null, false) in the onActivityCreated method of the fragment?
I have tried it, tested it and it seems to be working fine. But I'm not convinced that it is right.
Actually, checking the source code, you end up doing the same.
Fragment.getLoaderManager:
/**
* Return the LoaderManager for this fragment, creating it if needed.
*/
public LoaderManager getLoaderManager() {
if (mLoaderManager != null) {
return mLoaderManager;
}
if (mActivity == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mCheckedForLoaderManager = true;
mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, true);
return mLoaderManager;
}
mWho is basically the fragment ID.
final void setIndex(int index, Fragment parent) {
mIndex = index;
if (parent != null) {
mWho = parent.mWho + ":" + mIndex;
} else {
mWho = "android:fragment:" + mIndex;
}
}
The difference in Activity.getLoaderManager() is that who will be (root)
So even though you can do what you are asking, calling it directly from the Fragment might be a better approach
Activity source code
Fragment source code
Disclaimer: I only checked the source code in the latest version, but I don't expect it to be very different
May i ask why you are simply not retaining the Fragment? It seems that what you need is to create the Loader in the Fragment and create the fragment with setRetainInstance(true).
In this case remember to provide a TAG when you add the fragment.
This way the fragment will survive even to activity config changes and only the view will be recreated leaving your loader alive.
i have a listFragment this is my onActivityCreated:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
utenti=savedInstanceState.getParcelableArrayList("array");
}else{
threadutenti= (GetUtenti) new GetUtenti(this.getActivity(), utenti).execute();
}
When i rotate my device i have first savedInstanceState non null (i got it) but after onActivityCreated is called with onActivityCreated null!! why? i want get my object utenti on ListFragment and not on my activity..
This can happen if you're always creating a new fragment in your Activity#onCreate() callback.
If it's true, check this answer. Your Fragment#onActivityCreated will be called twice:
First time triggered by FragmentActivity#onStart's call to mFragments.dispatchActivityCreated(). It will pass the saved instance.
Second time triggered by FragmentActivity#onStart's call to mFragments.execPendingActions(). This time without saved instance (since it is being called in response to new Fragment addition).
I wrote Android JUnit test for Activity that instantiates fragments (actually tabs). During the test, when I try to do anything with these tabs, they crash because getActivity() method in them returns null. The actual application (not a test) never shows this behavior and fragment getActivity() always returns the right parent activity there. My test case looks like:
public class SetupPanelTest extends ActivityUnitTestCase<MyAct> {
FSetup s;
public SetupPanelTest() {
super(MyAct.class);
}
protected void setUp() throws Exception {
super.setUp();
startActivity(new Intent(), null, null);
final MyAct act = getActivity();
AllTabs tabs = act.getTabs();
String tabname = act.getResources().getString(R.string.configuration);
// This method instantiates the activity as said below
s = (FSetup) tabs.showTab(tabname);
FragmentManager m = act.getFragmentManager();
// m.beginTransaction().attach(s).commit();
// ... and even this does not help when commented out
assertTrue(s instanceof FSetup); // Ok
assertEquals(act, s.getActivity()); // Failure
}
public void testOnPause() {
// this crashes because s.getActivity == null;
s.onPause();
}
}
The AllTabs creates a fragment, then required, in this way:
FragmentManager manager = getFragmentManager();
Fragment fragment = manager.findFragmentByTag(tabname);
if (fragment == null || fragment.getActivity() == null) {
Log.v(TAG, "Instantiating ");
fragment = new MyFragment();
manager.beginTransaction().replace(R.id.setup_tab, fragment, tabname).commit();
....
Here, all fragments are initially placeholders that are later replaced by the actual fragments:
<FrameLayout
android:id="#+id/setup_tab"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
The logcat shows that the new fragment has been instantiated. In the same layout, there is also the previously mentioned AllTabs fragment that seems not having this problem (where and how it gets FragmentManager otherwise):
<TabWidget
android:id="#android:id/alltabs"
...
Most impressively, when I call attach directly on the fragment manager obtained on the right activity, this still has no effect. I tried to put five seconds delay (I have read that transaction may be delayed), I tried to call the rest of the test through runOnUiThread - nothing helps.
The question is that is need to do so to attach my fragments to the activity also during the test. I have fragment and I have activity, I cannot attach one to another.
Even if you call .commit() on transaction, it is still not done, fragments are attached only lazily.
FragmentManager m = activity.getFragmentManager();
m.executePendingTransactions();
This finally attaches all fragments to the activity. Seems redundant when running the application itself but required in JUnit test case.