I have a TabLayout with 4 tabs, what I want is to keep the first 3 tabs on ViewPager memory and recreate the fragment in the 4th tab everytime this tab is selected, is this possible?
This is my adapter:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private Fragment[] mFragments;
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
mFragments = new Fragment[4];
mFragments[0] = PlaceholderFragment.newInstance(0);
mFragments[1] = PlaceholderFragment.newInstance(1);
mFragments[2] = PlaceholderFragment.newInstance(2);
mFragments[3] = PlaceholderFragment.newInstance(3);
}
#Override
public Fragment getItem(int position) {
return mFragments[position];
}
#Override
public int getCount() {
// Show 4 total pages.
return 4;
}
#Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return null;
case 1:
return null;
case 2:
return null;
case 3:
return null;
}
return null;
}
}
I know I am a little late to this.
You can use the offscreenPageLimit on the ViewPagerInstance to specify the amount of pages to keep alive on either side. The default value is 1 (Also the min value is 1. So you can't have every fragment recreated. IMHO this is probably done for smooth animations)
Check this documentation link for details offscreenPageLimit https://developer.android.com/reference/android/support/v4/view/ViewPager#setoffscreenpagelimit
As specified in the docs, be careful when tweaking this property. It can make the user experience better or worse based on how and where to use it.
Now for the second part of your question about recreating only a particular fragment. Check this documentation link https://developer.android.com/reference/android/support/v4/app/Fragment#setUserVisibleHint(boolean)
This basically should be called whenever your fragment is made visible to the user or not.
public class YourFragmentThatNeedsToBeRecreated extends Fragment {
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
// write your refresh logic here
}
}
Though you might face problems with this approach. It doesn't work perfectly for some reason (Insert a rant about fragments here). You can try attaching and detaching your fragment in the refresh logic. But I hope there's a cleaner solution for it.
Related
I am trying to implement viewPager2 with variable number of fragments, and not sure on the correct / best approach and couldn't find any good examples to copy!
The fragments in my viewpager depend on the properties of the object being displayed - the first 2 are always present, but there are 4 more that may or may not be present - i.e. the FragmentStateAdapter does not know how many fragments there will be or which ones will be present until runtime.
My initial approach was to have a List that I passed to my FragmentStateAdapter which looked like this:
public class busDetailFragmentAdapter extends FragmentStateAdapter {
private final List<Fragment> fragmentList;
public busDetailFragmentAdapter(#NonNull Fragment fragmentActivity, List<Fragment> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
// return fragments at every position
#NonNull
#Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
#Override
public int getItemCount() {
return fragmentList.size();
}
}
I then simply added fragments to my list with tabFragments.add(new busDetailInfoFrag()); and notified the fragmentadapter that an item had been inserted.
This worked fine until I navigated away from the parent fragment and then used the back button - the app crashed with a "Fragment already added" error.
I found something in the docs creating about new fragments and not reusing fragment. So, I modified my adapter to use a list of strings rather than a list of fragments, and am looking up the position against this list and using a switch statement to create the correct fragment.
public class busDetailFragmentAdapter extends FragmentStateAdapter {
private final List<String> fragmentList;
public busDetailFragmentAdapter(#NonNull Fragment fragmentActivity, List<String> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
// return fragments at every position
#NonNull
#Override
public Fragment createFragment(int position) {
switch (fragmentList.get(position)){
case "info":
return new busDetailInfoFrag();
case "location":
return new busDetailLocationFrag();
//More cases as needed
default:
return new missingFrag();
}
}
#Override
public int getItemCount() {
return fragmentList.size();
}
}
This is working as I need it to, but just wondering if this is the best/correct approach or if there's a better way!
When I start the app everything works ok but when I rotate to landscape it crashes because in the Fragment there is a field that is NULL.
I dont use setRetainInstance(true) or adding Fragments to FragmentManagerI create new Fragments on app start and when app rotate.
In the Activity OnCreate() I create the Fragment and adding them to the viewPager like this.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ParentBasicInfoFragment parentBasicInfoFragment = new ParentBasicInfoFragment();
ParentUTCFragment parentUTCFragment = new ParentUTCFragment();
ParentEventsFragment parentEventsFragment = new ParentEventsFragment();
this.mFragments = new ArrayList<>();
this.mFragments.add(parentBasicInfoFragment);
this.mFragments.add(parentUTCFragment);
this.mFragments.add(parentEventsFragment);
this.viewpage.setOffscreenPageLimit(3);
setCurrentTab(0);
this.viewpage.setAdapter(new MainActivityPagerAdapter(getSupportFragmentManager(), this.mFragments));
}
Then I have a test button on the app that when I press it will do like
public void test(View view) {
((BaseFragment) MainActivity.this.mFragments.get(MainActivity.this.viewpage.
getCurrentItem())).activityNotifiDataChange("hello");
}
This will work and the current Fragments in the ViewPager have the method, activityNotifiDataChange() that are being called and all is ok.
When I rotate the app and do the same thing pressing the button the activityNotifiDataChange() is being called alright but there a null pointer exception because the ArrayList<Fragment> mFragment is now NULL.
HereĀ“s a small sample Android Studio project showing this behavior:
https://drive.google.com/file/d/1Swqu59HZNYFT5hMTqv3eNiT9NmakhNEb/view?usp=sharing
Start app and press button named "PRESS TEST", then rotate device and press the button again and watch the app crash
UPDATE SOLUTION thanks #GregMoens and #EpicPandaForce
public class MainActivityPagerAdapter extends PersistenPagerAdapter<BaseFragment> {
private static int NUM_ITEMS = 3;
public MainActivityPagerAdapter(FragmentManager fm) {
super(fm);
}
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return ParentBasicInfoFragment.newInstance(0, "Page # 1");
case 1:
return ParentUTCFragment.newInstance(1, "Page # 2");
case 2:
return ParentEventsFragment.newInstance(2, "Page # 3");
default:
return null;
}
}
}
public abstract class PersistenPagerAdapter<T extends BaseFragment> extends FragmentPagerAdapter {
private SparseArray<T> registeredFragments = new SparseArray<T>();
public PersistenPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
public T instantiateItem(ViewGroup container, int position) {
T fragment = (T)super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public T getRegisteredFragment(ViewGroup container, int position) {
T existingInstance = registeredFragments.get(position);
if (existingInstance != null) {
return existingInstance;
} else {
return instantiateItem(container, position);
}
}
}
The main problem I see with your app is your misunderstanding with how FragmentPagerAdapter works. I see this a lot and it's due to lack of good javadocs on the class. The adapter should be implemented so that getItem(position) returns a new fragment instance when called. And then getItem(position) will only be called by the pager when it needs a new instance for that position. You should not pre-create the fragments and pass then into the adapter. You should also not be holding strong references to the fragments from either your activity or from parent fragments (like ParentBasicInfoFragment). Because remember, the fragment manager is managing fragments and you are also managing fragments by newing them and keeping references to them. This is causing a conflict and after rotation, you are trying to invoke activityNotifiDataChange() on a fragment that is not actually initialized (onCreate() was not called). Using the debugger and tracking object IDs will confirm this.
If you change your code so that the FragmentPagerAdapter creates the fragments when they are needed and don't store references to fragments or lists of fragments, you will see much better results.
this.mFragments = new ArrayList<>();
Because this is wrong. You should never hold a reference to the fragment list if you are using ViewPager. Just return new instance of Fragment from getItem(int position) of FragmentPagerAdapter and you are good to go.
But to fix your code, you must delete mFragments entirely.
See https://stackoverflow.com/a/58605339/2413303 for more details.
Use setRetainInstance(true) is not a good approach. If you need to same some simple information such as: position of recyclerView, selected item of recyclerView, maybe some model(Parcelable) you could do it with method onSaveInstanceState / onRestoreInstanceState, there is one limitation is 1MB. Here is an example with animations how it works.
For more durable persistance use SharedPreferences, Room(Google ORM) or you could try to use ViewModel with LiveData (best approach some data which should live while user session).
//change in AndroidManifest.xml file,
<activity
android:name=".activity.YourActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="sensor"
/>
//YourActivity in which you defines fragment.
//may be it helps
Since in Android L the the Action bar navigation modes are deprecated I'm searching an other way to have the tab and I found that is possible to use the PagerTabStrip (android.support.v4.view.PagerTabStrip), so I created a FragmentPageAdapter in this way:
public class TitleAdapter extends FragmentPagerAdapter {
private final String titles[] = new String[] { "Home", "Events", "People", "Books"};
private final Fragment frags[] = new Fragment[titles.length];
Context context;
public TitleAdapter(FragmentManager fm) {
super(fm);
}
#Override
public CharSequence getPageTitle(int position) {
Log.v("TitleAdapter - getPageTitle=", titles[position]);
return titles[position];
}
#Override
public Fragment getItem(int position) {
Log.v("TitleAdapter - getItem=", String.valueOf(position));
//return frags[position];
switch (position) {
case 0:
return Home.newInstance(0, "Home");
case 1:
return Events.newInstance(1, "Events");
case 2:
return People.newInstance(2, "People");
case 4:
return Books.newInstance(3, "Books");
}
return null;
}
#Override
public int getCount() {
return frags.length;
}
}
The strange way that i see in LogCat is that the method getItem() is called 4 times when the mainActivity starts so I've to wait a lots because in each Tab there is a quite long list and this list is populated via HTTP request calling a web service.
I wish load only a fragment each times and not all. When i used actionbar.Tablistener it was possible but now the method is deprecated so is there a way to do that?
I set the adapter and the viewPager in the onCreate method in this way:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.prova_page_tab_stripes);
mViewPager = (ViewPager) findViewById(R.id.viewpager);
titleAdapter = new TitleAdapter(getSupportFragmentManager());
mViewPager.setAdapter(titleAdapter);
mViewPager.setOffscreenPageLimit(1);
}
The number of pages initialized depends on setOffscreenPageLimit function of ViewPager. As per android doc in http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setOffscreenPageLimit(int),
public void setOffscreenPageLimit (int limit)
Set the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. Pages beyond this limit will be recreated from the adapter when needed.
This is offered as an optimization. If you know in advance the number of pages you will need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting can have benefits in perceived smoothness of paging animations and interaction. If you have a small number of pages (3-4) that you can keep active all at once, less time will be spent in layout for newly created view subtrees as the user pages back and forth.
You should keep this limit low, especially if your pages have complex layouts. This setting defaults to 1.
Parameters
limit How many pages will be kept offscreen in an idle state.
Set it to 1 or keep it to default if you wan to limit pages to be retained.
But if you want to load the data only in one page at a time, you can determine when fragment becomes visible and then load the data (insted of loading in onCreateView.)
Refer this question: How to determine when Fragment becomes visible in ViewPager
I understand the lowest number I can give setOffscreenPageLimit(int) is 1. but I need to load one page at a time because memory problems.
Am i going to have to use the old style tabhost etc? or is there a way/hack I can make my viewPager load one page at a time?
My Adapter extends BaseAdapter with the ViewHolder patern.
I was having the same problem and I found the solution for it:
Steps:
1) First Download the CustomViewPager Class from this link.
2) Use that class as mentioned below:
In Java:
CustomViewPager mViewPager;
mViewPager = (CustomViewPager) findViewById(R.id.swipePager);
mViewPager.setOffscreenPageLimit(0);
In XML:
<com.yourpackagename.CustomViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/swipePager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Now only one page will be loaded at once.
P.S: As per the question's requirement, I have posted the solution for Viewpager. I haven't tried the same with TabLayout yet. If I will find any solution for that I will update the answer.
In this file, KeyEventCompat is used it may not found by the android studio because KeyEnentCompat class was deprecated in API level 26.0.0 so you need to replace KeyEventCompat to event for more details you can view
https://developer.android.com/sdk/support_api_diff/26.0.0-alpha1/changes/android.support.v4.view.KeyEventCompat
As far as I know, that is not possible when using the ViewPager. At least not, when you want your pages to be swipe-able.
The explaination therefore is very simple:
When you swipe between two pages, there is a Point when both pages need to be visible, since you cannot swipe between two things when one of those does not even exist at that point.
See this question for more: ViewPager.setOffscreenPageLimit(0) doesn't work as expected
CommonsWare provided a good explaination in the comments of his answer.
but I need to load one page at a time because memory problems.
That presumes that you are getting OutOfMemoryErrors.
Am i going to have to use the old style tabhost etc?
Yes, or FragmentTabHost, or action bar tabs.
or is there a way/hack I can make my viewPager load one page at a time?
No, for the simple reason that ViewPager needs more than one page at a time for the sliding animation. You can see this by using a ViewPager and swiping.
Or, you can work on fixing your perceived memory problems. Assuming this app is the same one that you reported on earlier today, you are only using 7MB of heap space. That will only result in OutOfMemoryErrors if your remaining heap is highly fragmented. There are strategies for memory management (e.g., inBitmap on BitmapOptions for creating bitmaps from external sources) that help address such fragmentation concerns.
My Adapter extends BaseAdapter with the ViewHolder patern.
BaseAdapter is for use with AdapterView, not ViewPager.
I have an Answer for this. The above said method setUserVisibleHint() is deprecated and you can use setMaxLifecycle() method. For loading only the visible fragment you have to set the behaviour to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT in the viewpager adapter. ie; in the Constructor. And for handling the fragment use onResume() method in the fragment.
In this way you can load only one fragment at a time in the viewpager.
public static class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
return ArrayListFragment.newInstance(position);
}
}
In Kotlin:
class MyAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT )
Also use with FragmentPagerAdapter (now deprecated) in same way
By using this method you can load one page at time in tab layout with view pager`
#Override
public void onResume() {
super.onResume();
if (getUserVisibleHint() && !isVisible) {
Log.e("~~onResume: ", "::onLatestResume");
//your code
}
isVisible = true;
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && isVisible) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
//your code
}
}, 500);
}
}
`
Override the setUserVisibleHint and add postDelayed like below in your every fragments.
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
if (isVisibleToUser)
Handler().postDelayed({
if (activity != null) {
// Do you stuff here
}
}, 200)
super.setUserVisibleHint(isVisibleToUser)
}
I can manage by this way and its working fine now for me.
First, copy in the SmartFragmentStatePagerAdapter.java which provides the intelligent caching of registered fragments within our ViewPager. It does so by overriding the instantiateItem() method and caching any created fragments internally. This solves the common problem of needing to access the current item within the ViewPager.
Now, we want to extend from SmartFragmentStatePagerAdapter copied above when declaring our adapter so we can take advantage of the better memory management of the state pager:
public abstract class SmartFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
// Sparse array to keep track of registered fragments in memory
private SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
public SmartFragmentStatePagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
// Register the fragment when the item is instantiated
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
// Unregister when the item is inactive
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
// Returns the fragment for the position (if instantiated)
public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
}
// Extend from SmartFragmentStatePagerAdapter now instead for more dynamic ViewPager items
public static class MyPagerAdapter extends SmartFragmentStatePagerAdapter {
private static int NUM_ITEMS = 3;
public MyPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
// Returns total number of pages
#Override
public int getCount() {
return NUM_ITEMS;
}
// Returns the fragment to display for that page
#Override
public Fragment getItem(int position) {
switch (position) {
case 0: // Fragment # 0 - This will show FirstFragment
return FirstFragment.newInstance(0, "Page # 1");
case 1: // Fragment # 0 - This will show FirstFragment different title
return FirstFragment.newInstance(1, "Page # 2");
case 2: // Fragment # 1 - This will show SecondFragment
return SecondFragment.newInstance(2, "Page # 3");
default:
return null;
}
}
// Returns the page title for the top indicator
#Override
public CharSequence getPageTitle(int position) {
return "Page " + position;
}
}
You actually don't need a custom ViewPager.
I had the same issue and I did like this.
Keep the setOffscreenPageLimit() as 1.
Use fragment's onResume and onPause lifecycle methods.
Initialize and free-up memories on these lifecycle methods.
I know this is an old post, but I stumbled upon this issue and found a good fix if your loading fragments. Simply, check if the user is seeing the fragment or not by overriding the setUserVisibleHint(). After that load the data.
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
getData(1, getBaseUrl(), getLink());
}
}
I have a ViewPager with 3 Fragments and my FragmentPagerAdapter:
private class test_pager extends FragmentPagerAdapter {
public test_pager(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
return fragments[i];
}
#Override
public long getItemId(int position) {
if (position == 1) {
long res = fragments[position].hashCode()+fragment1_state.hashCode();
Log.d(TAG, "getItemId for position 1: "+res);
return res;
} else
return fragments[position].hashCode();
}
#Override
public int getCount() {
return fragments[2] == null ? 2 : 3;
}
#Override
public int getItemPosition(Object object) {
Fragment fragment = (Fragment) object;
for (int i=0; i<3; i++)
if (fragment.equals(fragments[i])){
if (i==1) {
return 1; // not sure if that makes a difference
}
return POSITION_UNCHANGED;
}
return POSITION_NONE;
}
}
In one of the page (#1), I keep changing the fragment to be displayed. The way I remove the old fragment is like this:
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().remove(old_fragment1).commit();
And then just changing the value of fragments[1]
I found that I cannot really add or replace the new one or it will complain the ViewPager is trying to add it too with another tag... (am I doing something wrong here?)
All the fragments I display have setRetainInstance(true); in their onCreate function.
My problem is that this usually works well for the first few replacement, but then when I try to reuse a fragment, sometimes (I have not really figured out the pattern, the same fragment may be displayed several times before this happens) it will only show a blank page.
Here is what I have found happened in the callback functions of my Fragment I am trying to display when the problem happens:
onAttach is called (but at that time, getView is still null)
onCreateView is not called (that's expected)
onViewStateRestored is not called (why not?)
onResume is not called (I really thought it would...)
If it changes anything, I am using the support package, my activity is a SherlockFragmentActivity
EDIT (to answer Marco's comment):
The fragments are instantiated in the onCreate function of the Activity, I fill an ArrayList with those fragments:
char_tests = new ArrayList<Fragment>(Arrays.asList(
new FragmentOptionA(), new FragmentOptionB(), new FragmentOptionC()));
The I pick from that list to set fragments[1] (that's all done in the UI thread)
I fixed this by changing test_pager to extends FragmentStatePagerAdapter instead.
I am still confused as to what PagerAdapter should be used depending on the usage. The only thing I can find in the documentation says that FragmentPagerAdapter is better for smaller number of pages that would be kept in memory and FragmentPagerStateAdapter better for a larger number of pages where they would be destroyed and save memory...
When trying to do (fancy?) things with Fragments, I found FragmentStatePagerAdapter is better when pages are removed and re-inserted like in this case. And FragmentPagerAdapter is better when pages move position (see bug 37990)