Swapping list of fragments inside FragmentStatePagerAdapter - android

I'm having trouble swapping a list of fragments inside my FragmentStatePagerAdapter, and was wondering if anyone might have an idea. I was hoping to have the following feature;
main activity includes three tabs (restaurants, reviews, favorites), each containing a fragment with a list of restaurants
user can swipe between each tab to view a different list
main activity layout has a button. when user presses the button it will swap all the existing fragments with another list of fragments. The new list of fragments will be three fragments with restaurant location maps (google maps) instead of list of restaurants
I currently have the swipe tabs working with actionbar and view pager. I have some degree of success swapping out the list fragments with the map fragments, but the following problem occurs:
when I hit the button on the main page, the first (restaurants) tab becomes blank.
when I swipe to the next tab (reviews), the tab is populated with a map fragment
when I swipe to the third tab (favorites), the tab is empty again
My setting is the following:
Main activity extending FragmentActivity and implementing ActionBar.ITabListener
One RestaurantListFragment class is used for the restaurant list fragments, populated with different data for Search, Reviews, and Favorites
One RestaurantListMap class is used for the map list fragments, again populated with different data for each tab
One TabPagerAdapter class extending FragmentStatePagerAdapter
I've tried almost every post on stackoverflow about this topic, but so far with no success. Would anyone have any ideas? Code is attached below. Thanks.
//Main activity
public class Main : FragmentActivity, ActionBar.ITabListener
{
//Some generic code here, nothing special
//....//
//The button that triggers the fragments swap
private void BindCommands()
{
ListMapSwitchButton.Click += (sender, e) =>
{
_isList = !_isList;
ListMapSwitchButton.Text = _isList ? RESTAURANTS_MAP_TEXT : RESTAURANTS_LIST_TEXT;
mAdapter.SwapListMapFragments(viewPager);
};
}
}
public class TabsPagerAdapter : FragmentStatePagerAdapter {
private void PopulateFragments()
{
_fragments = new List<RestaurantFragmentBase> ()
{
new RestaurantListFragment (),
new RestaurantListFragment(),
new RestaurantListFragment()
};
}
public void SwapListMapFragments(ViewPager pager)
{
_fragments.Clear ();
_fragments.Add (
new RestaurantMapFragment()
);
_fragments.Add (
new RestaurantMapFragment()
);
_fragments.Add (
new RestaurantMapFragment()
);
NotifyDataSetChanged ();
pager.DestroyDrawingCache ();
}
public override int GetItemPosition (Java.Lang.Object itemObject)
{
return PositionNone;
}
public override Android.App.Fragment GetItem(int index)
{
return _fragments[index];
}
#region implemented abstract members of PagerAdapter
public override int Count {
get {
return 3;
}
}
#endregion
}
Note that this code is actually written in C# with Xamarin, but besides syntax differences it should pretty much be the same as Java. Let me know if any additional info might help.

Related

Loading Fragment UI on-demand

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().

Android ViewPager FragmentStatePagerAdapter issue

I'm building an app that uses a Viewpager with a FragmentStatePagerAdapter.
The FragmentStatePagerAdapter gets an array of objects witch is later used to map each object data to a fragment.
The adapter loads 3 fragments into memory (onCreateView is called 3 times when I'm on a page), it loads the current fragment and the next two.
I have the following issue:
I must change the content of the next fragment based on content change in the current fragment I'm in.
I've tried to modify the array in the FragmentStatePagerAdapter and then call notifydatasetchanged on the adapter, but the adapter doesn't load again the next page.
what is the best way to pull this off?
some code and scenario:
the current fragment I am on contains users data, the logged user can follow him or unfollow.
here is the onClick code:
if(mListener!=null)
mListener.onTweetUserFollowingStatusChanged(tweet.getUser());
the callback from listener in the activity with the viewpage:
#Override
public void onTweetUserFollowingStatusChanged(User user) {
DataManager.getInstance().onTweetUserFollowStatusChanged(user);
List<Tweet> affectedTweets = mPagerAdapter.getTweetsWithUser(user);
for (Tweet affectedTweet: affectedTweets
) {
boolean affectedTweetCurrentUserFollowStats = affectedTweet.isFollowingTweetUser();
affectedTweet.setFollowingTweetUser(!affectedTweetCurrentUserFollowStats);
}
mPagerAdapter.notifyDataSetChanged();
}
The next page containes the same user.
So if I follow him on the curent page I want the follow button from the next page to display "UNFOLLOW"
I have found the solution;
Original problem:
after updating the array in the FragmentStateAdapter I needed a way to update the UI of the already instantiated fragments in the Adapter.
So I after updating the array in the FragmentStateAdapter just call notifyDataSetChange() and add this code to your adapter.
mPagerAdapter.registerDataSetObserver(new DataSetObserver() {
#Override
public void onChanged() {
super.onChanged();
FragmentStatePagerAdapter fragmentPagerAdapter = (FragmentStatePagerAdapter) VP_tweets.getAdapter();
for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {
TweetFragment viewPagerFragment = (TweetFragment) VP_tweets.getAdapter().instantiateItem(VP_tweets, i);
if(viewPagerFragment != null && viewPagerFragment.isAdded()) {
viewPagerFragment.updateUI();
}
}
}
});

Replace a fragment within a Viewpager(notifyDataSetChanged does not work)

My MainActivity contains a viewPager.
In the MainActivity.java, I set the adapter for viewpager. The adapter extends FragmentStatePagerAdapter. The fragment I want to replace is a
cameraFragment. So when the user clicks on the switch camera button, I want to now show the camera fragment, this time with a front camera on.
On clicking the switch Camera button, I remove the fragment from the arraylist of fragments I had passed to the custom adapter. I add the new fragment and call notifydatasetchanged. However, this does not result in the new fragment being added. How do I achieve dynamic replacement of fragments within a viewpager which is backed my a custom fragment state pager adapter?
Code :
mainPageFragments = new ArrayList<>();
mainPageFragments.add(new ResultsFragment_());
mainPageFragments.add(DemoCameraFragment_.newInstance(false));
pagerAdapter = new MainViewPagerAdapter(getSupportFragmentManager(),mainPageFragments);
To replace the fragment : On receiving the related event I do,
mainPageFragments.remove(1);
if (event.getCameraState().equals(CameraSwitchButton.CameraTypeEnum.BACK)) {
mainPageFragments.add(DemoCameraFragment.newInstance(false));
} else {
mainPageFragments.add(DemoCameraFragment.newInstance(true));
}
// Not Working...
pagerAdapter.notifyDataSetChanged();
Adapter Code :
public class MainViewPagerAdapter extends FragmentStatePagerAdapter {
ArrayList<Fragment> fragmentsArray;
public MainViewPagerAdapter(FragmentManager fm, ArrayList<Fragment> fragmentsArray) {
super(fm);
this.fragmentsArray = fragmentsArray;
}
#Override
public Fragment getItem(int position) {
return fragmentsArray.get(position);
}
#Override
public int getCount() {
return fragmentsArray.size();
}
#Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
}
Your MainViewPagerAdapter.getItemPosition is the cause of your issue.
Default implementation always returns POSITION_UNCHANGED. For pager to remove your fragment you have to return PagerAdapter.POSITION_NONE for the fragments that are removed.
Additionally your current design contradicts with the idea of FragmentStatePagerAdapter. From the FragmentStatePagerAdapter documentation: "This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages."
Your current implementation holds all fragments in an array, and so defeats this mechanism. Correct implementation would be to create fragments in MainViewPagerAdapter.getItem method and let the adapter to handle fragments lifecycles as needed.
Thanks to #Okas. I made the following change to getItemPosition within my FragmentStatePagerAdapter subclass.
#Override
public int getItemPosition(Object object) {
if (object instanceof DemoCameraFragment_)
return POSITION_NONE;
return super.getItemPosition(object);
}
I added logs to the OnCreate of both my fragments to confirm if they were getting recreated or not. As per my requirement, only the second fragment is recreated.

Viewpager update and backstack

I have a view pager with 2 scrolling pages in my app. at first I populate it with two fragments.
In first fragment I have a button. clicking the button new adapter is created and view pager is populated with two different fragments. at the moment when I press back I exit from the app instead I want to restore previous state of the view pager. please help
For the first time:
ViewPagerAdapter pagerAdapter = new ViewPagerAdapter(getSupportFragmentManager(),nBank);
mViewpager.invalidate();
mViewpager.setAdapter(pagerAdapter);
Second time:
public void onListItemPressed(Currency objectCurrency) {
// TODO Auto-generated method stub
DetailPagerAdapter detaluriadapteri = new DetailPagerAdapter(getSupportFragmentManager());
mViewpager.setAdapter(detaluriadapteri);
}
One solution could be to implement onBackPressed:
#Override public void onBackPressed() {
if (mViewpager != null && mViewpager.getAdapter() instanceof DetailPagerAdapter) {
ViewPagerAdapter pagerAdapter = new ViewPagerAdapter(getSupportFragmentManager(),nBank);
mViewpager.invalidate();
mViewpager.setAdapter(pagerAdapter);
} else {
super.onBackPressed();
}
}
Though, I think it would be better to start a new activity using DetailPagerAdapter when onListItemPressed is called. This way the default behavior of android would be to navigate back to your main activity on backpress, currently your main activity could be getting too much responsibility. Thus, having a self contained activity handling the details part would also be easier to maintain as it might need different tabs, actionbar menu items, etc..
Could also be a Fragment containing a the details viewpager but I have had trouble implementing this myself. Should be possible though and my trouble might be caused by some of the libraries I use.

Notifying activity of fragment swiped into view via a pager

I am having trouble implementing a feature in my Android app.
Here's the setup:
ItemPagerActivity: An activity that contains a fragment that displays a pager.
ItemPagerFragment: The fragment containing a pager that loads other fragments. A cursor is used to load the fragments.
ItemFragment: The fragment in the pager, which performs an asynchronous task to load its data.
What I want is the following:
as a I swipe pages, the data in the currently displayed ItemFragment is communicated to the ItemPagerActivity (specifically, the name of the item will be used as the activity's title).
I've defined a listener in ItemFragment that notifies when the data is loaded:
public class ItemFragment ... {
public interface OnItemLoadedListener {
public void onItemLoaded(Item item);
}
private Collection<OnItemLoadListener> listeners;
private class LoadItemTask extends AsyncTask<...> {
...
public void onPostExecute(Item item) {
notifyItemLoaded(item);
...
}
}
}
If this fragment was wrapped by an Activity, then I could set the activity's title simply by doing the following:
public class ItemActivity {
public void onCreate(...) {
...
ItemFragment fragment = new ItemFragment();
fragment.registerItemLoadedListener(new ItemLoadedListener() {
public void onItemLoaded(Item item) {
setTitle("Item: " + item.getName());
}
});
...
}
}
So that's easy enough, and works as expected: when the activity starts, it creates the fragment, which loads the item, which notifies the activity, and the title is updated correctly.
But with ItemPagerFragment, the fragments are loaded pre-emptively: swiping to Fragment 3 may mean that Fragment 4 and Fragment 5 are created. Receiving notifications from the ItemFragment class when items are loaded is not correct here because the fragment displayed may not match the fragment that performed the last load.
Now the ViewPager class has a OnPageChangeListener which could be a solution: when I swipe, this listener is invoked with the current page number. From that page number, I need to (somehow) get the fragment representing that page from the adapter, get the Item data out of the fragment, and notify listeners that the Item is now loaded:
public class ItemPagerFragment ... {
private Collection<OnItemLoadedListener> listeners;
public View onCreateView(...) {
...
ViewPager pager = (ViewPager) view.findViewById(R.id.pager):
pager.setOnPageChangeListener(new OnPageChangeListener() {
public void onPageChange(int pageNumber) {
ItemFragment fragment = getItemFragment(pageNumber);
Item item = fragment.getLoadedItem();
notifyItemLoaded(item);
}
});
...
}
}
The ItemPagerActivity class would then register as a listener on the ItemPagerFragment class as follows:
class ItemPagerActivity ... {
public void onCreate(...) {
...
ItemPagerFragment fragment = new ItemPagerFragment();
fragment.registerOnItemLoadedListener(new OnItemLoadedListener() {
public void onItemLoaded(Item item) {
setTitle("Item: " + item.getName());
}
});
...
}
}
This looks good, but there are a number of problems:
The OnPageChangeListener may be invoked before a fragment has loaded its data (i.e., the fragment is swiped into view before the item has asynchronously loaded). So the call to fragment.getLoadedItem() may return null.
The OnPageChangeListener is not invoked for the initial page (only when a page changes, e.g. after a swipe action) so the activity title will be incorrect for the initial page.
The ViewPager class allows for only one OnPageChangeListener. This is a problem because I am also using the ViewPageIndicator library, which wants to assign a listener to the ViewPager.
I'm assuming that this pattern (notifying the activity of the data in a fragment that has been swiped into view) might be common, so I am wondering if there are any good solutions for this pattern, and to the three specific problems that I have identified above.
...so I am wondering if there are any good solutions for this pattern,
and to the three specific problems that I have identified above.
I don't know if I would call it a pattern but the OnPageChangeListener is the way to go.
The OnPageChangeListener may be invoked before a fragment has loaded
its data (i.e., the fragment is swiped into view before the item has
asynchronously loaded). So the call to fragment.getLoadedItem() may
return null.
First, your code should handle the "no data available situation" from the start. Your AsyncTasks will have the job of loading the data and also update the title only if the fragment for which they are working is the visible one(a position field in the ItemFragment tested against the ViewPager's getCurrentItem() method). The OnPageChangeListener will handle the update of the title after the data was loaded, as the user switches between pages and the data is available(it will return null if no data is available). To get the ItemFragment for a certain position you could use the code below:
ItemFragment itf = getSupportFragmentManager()
.findFragmentByTag(
"android:switcher:" + R.id.theIdOfTheViewPager + ":"
+ position);
if (itf != null) {
Item item = fragment.getLoadedItem();
notifyItemLoaded(item);
}
The OnPageChangeListener is not invoked for the initial page (only
when a page changes, e.g. after a swipe action) so the activity title
will be incorrect for the initial page.
See above.
The ViewPager class allows for only one OnPageChangeListener. This is
a problem because I am also using the ViewPageIndicator library, which
wants to assign a listener to the ViewPager
I admit I don't have much knowledge on the ViewPagerIndicator library but at a quick look on its site I saw:
(Optional) If you use an OnPageChangeListener with your view pager you
should set it in the indicator rather than on the pager directly.
titleIndicator.setOnPageChangeListener(mPageChangeListener);
I don't see where is the limitation.
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.

Categories

Resources