I am using 4 fragments inside a ViewPager ,as ViewPager load the previous and next fragment in advance ,and no lifecycle method is called when navigating between fragments.
So is there any way to detect when Fragment is actually visible.
Thanks in Advance.
as per #Matt's answer setUserVisibleHint is deprecated
so here is alternative way for this.
#Override
public void setMenuVisibility(boolean isvisible) {
super.setMenuVisibility(isvisible);
if (isvisible){
Log.d("Viewpager", "fragment is visible ");
}else {
Log.d("Viewpager", "fragment is not visible ");
}
}
Of course. Assuming that viewPager is your instance of the ViewPager, use: viewPager.getCurrentItem().
Within your Fragment you can check if its instance is visible to the user like so:
#Override
public void setUserVisibleHint(boolean visible) {
super.setUserVisibleHint(visible);
if (visible) {
Log.i("Tag", "Reload fragment");
}
}
Always make sure that you search for answers throughly before asking your question. For instance, the first place you should check would be: https://developer.android.com/reference/android/support/v4/view/ViewPager.html
You can use viewPager.getCurrrentItem() to get the currently selected index, and from that you should be able to extrapolate which fragment is shown. However what you probably want is to use addOnPageChangeListener() to add an OnPageChangeListener. This will let you keep track of what page is selected, as it's selected by implementing the onPageSelected(int selected) method.
Did you try the isVisible method in the fragment?
Nowadays you can override androidx.fragment.app.onResume and androidx.fragment.app.onPauseto detect if it is visible or not respectively.
Related
I'm using FragmentStatePagerAdapter, ViewPager.
I'm going to use onSaveInstanceState by overriding to save some states like cursor position of EditText in every fragment.
But when I choose first fragment and next choose second fragment, the onSaveInstanceState of first fragment is not called. If I choose first and next choose third fragment, then the onSaveInstanceState of the first fragment is called.
In this case of choosing first fragment and next second fragment, even the onPause of the first fragment is not called.
What's the reason? How can I solve this problem? I have researched about this problem whole day. But I haven't found solution and correct reason yet.
onSaveInstanceState has cases that it can be called, but how about onPause? Why doens't onPause be called?
I found a solution. I used setUserVisibleHint.
In fragment, I wrote save and restore logic in setUserVisibleHint.
It works well.
This is called when fragment is shown or hidden.
Also I used onViewStateRestored, onSaveInstanceState together for being destroyed cases.
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser) {
if (mInputFrom != null) {
if(isToFocus) {
mInputFrom.requestFocus();
mInputFrom.setSelection(fromCursor);
} else {
mInputOut.requestFocus();
mInputOut.setSelection(toCursor);
}
}
} else {
if (mInputFrom != null) {
fromCursor = mInputFrom.getSelectionStart();
toCursor = mInputOut.getSelectionStart();
}
}
}
setUservisibleHint was deprecated.
So other option is that we can use constructor of FragmentStatePagerAdapter(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
If we call super's constructor like above in constructor of our customized Adapter that extends FragmentStatePagerAdapter, then onPause, onResume of fragment will be called for every case of being hidden or shown.
First option is suitable for my case I think. So I used first option and has solved perfectly.
Problem:
I am currently running into a problem where my app is trying to load too many fragments when it opens for the first time.
I have BottomNavigationView with ViewPager that loads 4 fragments - each one of the Fragment contains TabLayout with ViewPager to load at least 2 more fragments.
As you can imagine, that is a lot of UI rendering (10+ fragments) - especially when some of these fragments contain heavy components such as calendar, bar graphs, etc.
Currently proposed solution:
Control the UI loading when the fragment is required - so until the user goes to that fragment for the first time, there is no reason to load it.
It seems like it's definitely possible as many apps, including the Play Store, are doing it. Please see the example here
In the video example above - the UI component(s) are being loaded AFTER the navigation to the tab is completed. It even has an embedded loading symbol.
1) I am trying to figure out how to do exactly that - at what point would I know that this fragment UI need to be created vs it already is created?
2) Also, what is the fragment lifecycle callback where I would start the UI create process? onResume() means UI is visible to the user so loading the UI there will be laggy and delayed.
Hope this is clear enough.
EDIT:
I'm already using the FragmentStatePagerAdapter as ViewPager adapter. I noticed that the super(fm) method in the constructor is deprecated now:
ViewPagerAdapter(FragmentManager fm) {
super(fm); // this is deprecated
}
So I changed that to:
ViewPagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT: Indicates that only the current fragment will be in the Lifecycle.State.RESUMED state. All other Fragments are capped at Lifecycle.State.STARTED.
This seems useful as the onResume() of the Fragment will only be called when the Fragment is visible to the user. Can I use this indication somehow to load the UI then?
The reason your app loads multiple Fragments at the startup is most probably, you're initializing them all at once. Instead, you can initialize them when you need them. Then use show\ hide to attach\ detach from window without re-inflating whole layout.
Simple explanation: You'll create your Fragment once user clicks on BottomNavigationView's item. On clicked item, you'll check if Fragment is not created and not added, then create it and add. If it's already created then use show() method to show already available Fragment and use hide() to hide all other fragments of BottomNavigationView.
As per your case show()/hide is better than add()/replace because as you said you don't want to re-inflate the Fragment when you want show them
public class MainActivity extends AppCompatActivity{
FragmentOne frg1;
FragmentTwo frg2;
#Override
public boolean onNavigationItemSelected(MenuItem item){
switch(item.getId()){
case R.id.fragment_one:
if (frg2 != null && frg2.isAdded(){
fragmentManager.beginTransaction().hide(frg2).commit();
}
if(frg1 != null && !frg1.isAdded){
frg1 = new FragmenOne();
fragmentManager.beginTransaction().add(R.id.container, frg1).commit();
}else if (frg1 != null && frg1.isAdded) {
fragmentManager.beginTransaction().show(frg1).commit();
}
return true;
case R.id.fragment_two:
// Reverse of what you did for FragmentOne
return true;
}
}
}
And for your ViewPager as you can see from the example you're referring to; PlayStore is using setOffscreenPageLimit. This will let you choose how many Views should be kept alive, otherwise will be destroyed and created from start passing through all lifecycle events of the Fragment (in case view is Fragment). In PlayStore app's case that's probably 4-5 that why it started loading again when you re-selected "editor's choice" tab. If you do the following only selected and neighboring (one in the right) Fragments will be alive other Fragments outside screen will be destroyed.
public class FragmentOne extends Fragment{
ViewPager viewPager;
#Override
public void onCreateView(){
viewPager = .... // Initialize
viewpAger.setOffscreenPageLimit(1); // This will keep only 2 Fragments "alive"
}
}
Answer to both questions
If you use show/hide you won't need to know when to inflate your view. It will be handled automatically and won't be laggy since it's just attaching/detaching views not inflating.
It depends upon how you initialize your fragment in your activity. May be you are initializing all your fragment in onCreate method of your activity instead of that you can initialize it when BottomNavigation item is selected like below :
Fragment one,two,three,four;
#Override
public boolean onNavigationItemSelected(MenuItem item){
Fragment fragment;
switch(item.getId()){
case R.id.menu_one:{
if(one==null)
one = Fragment()
fragment = one;
break;
}
case R.id.menu_two:{
if(two==null)
two = Fragment()
fragment = two;
break;
}
}
getFragmentManager().beginTransaction().replace(fragment).commit();
}
To decide how many page is load in you view pager at one time you can use :
setOffscreenPageLimit.
viewPager.setOffscreenPageLimit(number)
To get the resume and pause functionality on fragments you can take an example from this link.
Please try this.
i was worked with the same kind of the Application, There were multiple tabs and also Tabs have multiple inner tabs.
i was used the concept of ViewPager method, In which there is one method of onPageSelected() for that method we were getting the page position.
By the Use of this position we are checking the current Fragment and called their custom method that we created inside that fragment like onPageSelected() defined inside that fragment.
With this custom method onPageSelected() inside the Fragment we checked that weather the list are available or not if list have data then we are not making the call of Api otherwise we are calling the Api and loading that list.
I think you have same kind of requirement to follow if your Tabs have inner Tab or viewpager you can follow same concept inside of that so if your current fragment of viewpager method onpageSelected called at that time your viewpager fragment initialized.
you have to call just initialization like data binding or view initialization need to be called in onCreate() method and other list attachment and api call to be managed by the custom method onPageSelected that will be called based on ViewPager onPageSelected.
let me Know if you need any help for same.
You can try to have Fragments with FrameLayouts only in ViewPager. The actual Fragments could be added to FrameLayout in onResume() (after checking if this Fragment isn't already attached). It should work if BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT works as expected.
I would recommend you use BottomNavigationView.setOnNavigationItemSelectedListener to toggle between the fragment UI whenever it is needed.
navigationView.setOnNavigationItemSelectedListener(item -> {
switch(item.getItemId()) {
case R.id.item1:
// you can replace the code findFragmentById() with findFragmentByTag("dashboard");
// if you only have one framelayout to hold the fragment
fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = new ExampleFragment();
// if the fragment is identified by tag, add another
// argument to this method:
// replace(R.id.fragment_container, fragment, "dashboard")
getSupportFragmentManager().begintransaction()
.replace(R.id.fragment_container, fragment)
.commit();
}
break;
}
}
The idea is simple, when the user swipes or selects a different tab, the fragment that was visible is replaced by the new fragment.
Just load fragments one by one. Create the main fragment layout with many placeholders and stubs and then just load them in the order you like.
Use FragmentTransaction.replace() from the main fragment after it loads.
Have you tried the setUserVisibleHint() method of a fragment
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if(isVisibleToUser){
// Do you stuff here
}
}
This will only get called when a fragment is visible to the user
How about you maintain just one ViewPager? Sounds crazy? In that case, you just change the dataset of PagerAdapter when you switch between the bottom tabs. Let's see how you can accomplish this,
As you mentioned, you have 4 fragments, which are assigned to each individual tabs of the bottom navigation view. Each performs some redundant work i.e. holding a viewPager with tab layout and setting the same kind of adapters. So, if we can combine these 4 redundant tasks into one then we will be able to get rid of 4 fragments. And as there will be just one viewPager with one single adapter then we will be able to reduce the fragment loading count from ~10 to 2 if we set offScreenPageLimit to 1. Let's see some example,
activity.xml should look like
<LinearLayout>
<TabLayout />
<ViewPager />
<BottomNavigationView />
</LinearLayout>
It's optional but I would recommend to create a base PagerFragment abstract class with abstract method getTabTitle()
public abstract class PagerFragment extends Fragment {
public abstract String getTabTitle();
}
Now it's time to make our PagerAdapter class
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
public Map<Integer, List<PagerFragment>> map = ...; // If you are concerned about memory then I could recommend to store DataObject instead of PagerFragment and instantiate fragment on demand using that data.
public int currentTabId = R.id.first_bottom_tab_id;
private List<PagerFragment> getCurrentFragments() {
return map.get(currentTabId);
}
public void setCurrentTabId(int tabId) {
this.currentTabId = tabId;
}
public SectionsPagerAdapter(FragmentManager manager) {
super(manager);
}
#Override
public Fragment getItem(int position) {
return getCurrentFragments().get(position);
}
#Override
public int getCount() {
return getCurrentFragments().size();
}
#Override
public int getItemPosition(#NonNull Object object) {
return POSITION_NONE;
}
#Override
public CharSequence getPageTitle(int position) {
return getCurrentFragments().get(position).getTabTitle();
}
}
And finally, in Activity
SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(pagerAdapter);
viewPager.setOffscreenPageLimit(1);
viewPagerTab.setViewPager(viewPager);
bottomNavigationView.setOnNavigationItemSelectedListener(menuItem -> {
pagerAdapter.setCurrentTabId(menuItem.getItemId())
pagerAdapter.notifyDataSetChanged();
viewPagerTab.setViewPager(viewPager);
}
This is the basic idea. You can mix some of your own ideas with it to make a wonderful result. Let me know if it is useful?
UPDATE
Answer to your questions,
I think with my solution you can achieve exactly the same behavior of the video as I already did it in a project. In my solution, if you set offset page limit to 1 then only adjacent fragment's is created in advance. So, fragment creation will be handled by adapter and viewpager you don't need to worry about it.
In my above solution, you should create UI in onCreateView().
I have 10 tabs in my activity. As soon as I open the activity the first fragment gets displayed but the method (AsyncTask) present in the next fragment gets called. And if I go to the next tab say 3rd tab then the method present in the 4th fragment gets called and so on.
I don't understand this behavior. Please help!
You must know how the viewPager works populating the fragment in the different positions
When you start on the position 0, then the fragment on the position 0 and the one of the position 1 are created.
Then when you swipe to the position 1 the fragment on the 2 position is created, so you have now the three fragments created on the different positions (0,1,2..assuming you have only 3 pages on the viewPager).
We swipe to the position 2, the last one, and the fragment on the first position (0) get destroy, so we have now the fragments on the positions 2 and 3.
This is how Fragment LifeCycle Works
you can set mViewPager.setOffscreenPageLimit(2); // 2 is just an example to limit it
If you want some code to execute when Fragment become Visible to User add part of code in setUserVisibleHint method
By default it is viewpager.setOffscreenPageLimit(1) , meaning View pager will by default load atleast 1 on the right and one on the left tab of current tab.
It is done so, mostly because there is a point when you slide viewpager, when certain area of both tabs is visible. For those smooth transitions preloading is required.
You cannot set it viewpager.setOffscreenPageLimit(0).
The only way out is to use this method setUserVisibleHint
add this to your fragment
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
// load data here
}else{
// fragment is no longer visible
}
}
This will be called only when that particular tab is visible to user, so only then you can call all loading function.
check sample example
Put your AsyncTask method inside this.
You can override setMenuVisibility like this:
#Override
public void setMenuVisibility(final boolean visible) {
if (visible) {
//execute AsyncTask method
}
super.setMenuVisibility(visible);
}
Happy coding!!
Override below method and move your code for Aync task into this instead of onStart() or onCreateView.
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
// Load your data here or do network operations here
isFragmentLoaded = true;
}
}
I'm using a ViewPager to display 2 Fragments as tabs. Once the according activity is loaded, both fragments are loaded immediatly, while only the first one is visible to the user.
Therefore view.isShown() is not sufficent for testing, as this method returns true for the second fragment which is not visible to the user.
ViewAsserts.assertOnScreen(decorView, view) seems to behave the same way and is therefore useless for solving this problem.
I'm aware that some similar questions have been asked, but none of their answers is satisfying my needs. So how to test this behavior (using Robotium)?
Solution:
I solved it according to Leon's suggestion by using a flag within the fragment like this:
private static boolean isVisibleToUser = false;
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
MyFragment.isVisibleToUser = isVisibleToUser;
}
public static boolean isVisibleToUser() {
return isVisibleToUser;
}
implementing it as a static method I can use it in my test this way:
assertTrue(MyFragment.isVisibleToUser());
the only drawback to this solution is that I have to implement these 2 methods in every single Fragment I want to test this way... any improvements?
You could override setUserVisibleHint inside your fragment like this:
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
//Fragment is selected in ViewPager
//Put your "on appear" validation/loading here
}
}
This method will fire every time you show or hide the fragment in the ViewPager.
As opposed to view.isShown() this method does take a "loaded but not visible" state into account.
use OnPageChangedListener to detect changes and maintain a reference to the currently visible fragment/page.
http://developer.android.com/reference/android/support/v4/view/ViewPager.OnPageChangeListener.html
Alternatively GetCurrentItem() may work for you as detailed here: How do you get the current page number of a ViewPager for Android?
I have a ViewPager, each page is a Fragment view. I want to test if a fragment is in a visible region. the Fragment.isVisible only test
the fragment is attached to a activity
the fragment is set to visible
the fragment has been added to a view
The ViewPager will create 3 (by default) fragment and all three of them meet the above criteria, but only one is actually visible to the user (the human eyes)
This is what I use to determine the visibility of a fragment.
private static boolean m_iAmVisible;
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
m_iAmVisible = isVisibleToUser;
if (m_iAmVisible) {
Log.d(localTAG, "this fragment is now visible");
} else {
Log.d(localTAG, "this fragment is now invisible");
}
}
You're right there is a better way to do this!
Have a look at the FragmentPagerAdapter javadoc online and you'll see there is a method setPrimaryItem(ViewGroup container, int position, Object object):void doing exactly what you need.
From the javadoc
public void setPrimaryItem (ViewGroup container, int position, Object object)
Called to inform the adapter of which item is currently considered to
be the "primary", that is the one show to the user as the current
page.
Parameters container The containing View from which the page will be
removed. position The page position that is now the primary.
object The same object that was returned by instantiateItem(View,
int).
Note on scroll state
Now if you implement this and start debugging to get a feel of when exactly this is called you'll quickly notice this is triggered several times on preparing the fragment and while the user is swiping along.
So it might be a good idea to also attach a ViewPager.OnPageChangeListener and only do what has to be done once the viewpagers scroll state becomes SCOLL_STATE_IDLE again.
For my purposes, it worked to use ViewPager.OnPageChangeListener.onPageSelected() in conjunction with Fragment.onActivityCreated() to perform an action when the Fragment is visible. Fragment.getUserVisibleHint() helps too.
I'm using "setMenuVisibility"-Method for resolving this Problem. As every Fragment can have actionbar-items this is the part where you can determine which Fragment is currently visible to the user.
#Override
public void setMenuVisibility(final boolean visible) {
super.setMenuVisibility(visible);
if (!visible) {
//not visible anymore
}else{
yay visible to the user
}
}
What is wrong with using getView().isShown() to find out if a Fragment is actually visible?
isVisible()
Can still return true even if the fragment is behind an activity.
I'm using the following:
if (getView() != null && getView().isShown()) {
//your code here
}
If you know what "page" each fragment is attached to you could use ViewPager.getCurrentItem() to determine which fragment is "visible".
In my case i a have to do some work on the first fragment when the fragment is visible to the user
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(viewPager.getAdapter() instanceof YourPager)
{
Fragment fragemnt=((YourPager)viewPager.getAdapter()).getFragment(0); //getFragment(int index) custom method
if( fragemnt instanceof YourFragment)
{
((YourFragment)fragemnt).methodWhochShouldBeCalledAfterUIVisible();
}
}
}
setUserVisibleHint probably may not be called, onHiddenChanged may be called not every time when another fragment is being closed. So, you may rely on onResume (and onPause), but it is usually called too often (for example, when you turn on a device screen). Also in some situations it is not called, you should manage current fragment in host activity and write:
if (currentFragment != null) {
currentFragment.onResume();
}
Kotlin:
if (userVisibleHint) {
// the fragment is visible
} else {
// the fragment is not visible
}
Java
if (getUserVisibleHint()) {
// the fragment is visible
} else {
// the fragment is not visible
}
https://developer.android.com/reference/android/app/Fragment.html#getUserVisibleHint()
https://stackoverflow.com/a/12523627/2069407