How to prevent creating 2 instances of same fragment? - android

I have an activity (MainActivity) that contains MasterFragment which contains a viewpager with FragmentA and FragmentB in portrait screen orientation.
In landscape mode the viewpager contains only FragmentA on left side of a split screen, with FragmentB on the right side.
So basically FragmentB is moved to the right of the viewpager in landscape mode.
Although FragmentB is only shown once in each rotation, two instances are created at the same time after rotation.
The problem is that FragmentB is in reality a map, and I need to prevent 2 instances to be created at the same time. I need the first instance to be destroyed before the next instance is created.
What happens is the FragmentStateManager recreates FragmentB when calling setContentView in MainActivity.
How do I prevent that?
One solution would be to use super.onCreate(null) in MainActivity, but that is clearly an overkill.
How can I prevent recreating fragments in ViewPager2?
Another solution would be to use the recreated fragment instance and move it from the viewpager to the framlayout and vice versa. How can I move it?
MasterFragment.java
public class MasterFragment extends Fragment
{
NewPagerAdapter mSectionsPagerAdapter;
ViewPager2 mViewPager;
boolean mSplitView;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.masterfragment, container, false);
if (isLandScape())
{
mSplitView = true;
getChildFragmentManager().beginTransaction().replace(R.id.container, new FragmentB(), FragmentB.TAG).commit();
}
else if (isLandScape())
{
LinearLayout masterlayout = view.findViewById(R.id.masterlayout);
masterlayout.removeViewAt(1);
}
mViewPager = view.findViewById(R.id.pager);
mSectionsPagerAdapter = new NewPagerAdapter(getChildFragmentManager(), getLifecycle());
mViewPager.setOffscreenPageLimit(7);
mViewPager.setAdapter(mSectionsPagerAdapter);
return view;
}
public boolean isLandScape()
{
int orientation = getResources().getConfiguration().orientation;
return orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public boolean backOnePage()
{
if(mViewPager == null)
return false;
int page = mViewPager.getCurrentItem();
if(page > 0)
{
mViewPager.setCurrentItem(page - 1);
return true;
}
return false;
}
public void viewFragmentB()
{
if(!mSplitView)
mViewPager.setCurrentItem(1);
}
public class NewPagerAdapter extends FragmentStateAdapter
{
public NewPagerAdapter(#NonNull FragmentManager fragmentManager, #NonNull Lifecycle lifecycle)
{
super(fragmentManager, lifecycle);
}
#NonNull
#Override
public Fragment createFragment(int position)
{
if(position == 0)
return new FragmentA();
return new FragmentB();
}
#Override
public int getItemCount()
{
return mSplitView ? 1 : 2;
}
}
}
masterfragment.xml (Portrait)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff000000"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
/>
</LinearLayout>
masterfragment.xml (Landscape)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/masterlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:background="#ff000000">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.5"
/>
<FrameLayout
android:id="#+id/container"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.5"/>
</LinearLayout>
MainActivity.java
public class MainActivity extends FragmentActivity
{
MasterFragment mMasterFragment;
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
Fragment fragment = savedInstanceState != null
? getSupportFragmentManager().getFragment(savedInstanceState, "MasterFragment")
: null;
mMasterFragment = fragment instanceof MasterFragment
? (MasterFragment)fragment
: (MasterFragment)getSupportFragmentManager().findFragmentById(R.id.masterfragment);
}
#Override
public void onBackPressed()
{
if(mMasterFragment != null && mMasterFragment.backOnePage())
return;
super.finish();
}
}
FragmentA
public class FragmentA extends Fragment
{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_home, container, false);
view.findViewById(R.id.title).setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View view)
{
MasterFragment.getInstance().viewFragmentB();
}
});
return view;
}
}
FragmentB
public class FragmentB extends Fragment
{
public static String TAG = "OrderFragment";
static int COUNTER;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_crew, container, false);
COUNTER++; // COUNTER BECOMES 2
return view;
}
#Override
public void onDestroy()
{
COUNTER--;
super.onDestroy();
}
}
main.xml
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.mobile.MasterFragment"
android:id="#+id/masterfragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

I think your problem is that you're always doing new FragmentB() or new Fragment...() instead of checking if it's already in the fragmentManager.
You have to do something like (please excuse my kotlin pseudocode)
var fragmentB = fragmentManager.findFragmentByTag("FragB")
if fragmentB == null {
fragmentB = // create new instance
}
fragmentManager.replace(..., fragmentB, "FragB") //use the same tag you'll use later to search for it

You need to use setRetainInstance in fragment that you want to retain over Activity configuration change.
Source
Edit
Thanks #martin for pointing that setRetainInstance is deprecated.
The requirement to prevent fragment recreation over configuration change is unachievable as per my understanding. I would suggest to maintain the fragment state via view model instance as per Android Doc's suggestion

Seems I need to answer my own question.
As some mentioned this is not directly supported by the Android platform.
Some possibilities are:
super.onCreate(null) in MainActivity (which will disable all state restore)
Write custom viewpager/adapter that omit the fragment in saving state (a lot of work)
After rotation, move fragment from/to Viewpager to/from Framelayout, but this will require customer viewpager/adapter that allows removing fragment view without destroying it (see moving view Android Fragment - move from one View to another?), which is presumably a lot of work

Related

Cannot see Fragments in ViewPager

Although I can scroll in the ViewPager, the physical fragments themselves are not visible for some reason.
FYI - I have an Activity with tabs using a ViewPager. Inside the first tab/fragment, I have the code below. So essentially, I have an App with a ViewPager controlling the tabs and there's another ViewPager inside one of the Tabs which is to control a bunch of images. I have tested the Fragment in question by putting it inside the top level view pager and it works perfectly fine! It's only when I put it inside the view pager in question does it not render anything...
So, this is the hierarchy for a better understanding:
MainActivity has a ...
ViewPager (3 fragments showing the tab content) has a ...
1st TabFragment has a ...
ViewPager (3 fragments showing images) <--- I can't see these, but I can swipe on them for some reason.
Unfortunately... None of the Fragments are visible, HOWEVER... I can still swipe for some reason to the last fragment...Just nothing is visibly shown...
Layout with the ViewPager inside:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="#+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Code with ViewPager
final List<Fragment> imageFragments = new ArrayList<>();
for (final UserImage userImage : user.getImages()) {
final SizeImage processed = userImage.getProcessed();
imageFragments.add(UserImageFragment.newInstance(processed.getFullsize()));
}
final ImagesPagerAdapter imagesAdapter = new ImagesPagerAdapter(getFragmentManager(), imageFragments);
viewPager.setAdapter(imagesAdapter);
The PagerAdapter:
public class ImagesPagerAdapter extends FragmentStatePagerAdapter {
#NonNull
private List<Fragment> fragments;
public ImagesPagerAdapter(#NonNull final FragmentManager fragmentManager,
#NonNull final List<Fragment> fragments) {
super(fragmentManager);
this.fragments = fragments;
}
#Override
public Fragment getItem(final int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
}
The Fragment:
public class UserImageFragment extends Fragment {
#BindView(R.id.image_view)
ImageView imageView;
public static UserImageFragment newInstance(final String url) {
final UserImageFragment fragment = new UserImageFragment();
Bundle args = new Bundle();
args.putString("url", url);
fragment.setArguments(args);
return fragment;
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_user_image, container, false);
ButterKnife.bind(this, view);
final Bundle arguments = getArguments();
if (arguments != null) {
final String url = arguments.getString("url", null);
if (url != null) {
final Uri uri = Uri.parse(url);
Picasso.with(getActivity()).load(url).into(imageView, new Callback() {
#Override
public void onSuccess() {
Toast.makeText(getActivity(), "Success", Toast.LENGTH_SHORT).show();
}
#Override
public void onError() {
Toast.makeText(getActivity(), "Fail", Toast.LENGTH_SHORT).show();
}
});
}
}
return view;
}
}
And here is xml code:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/md_red_900">
<ImageView
android:id="#+id/image_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#color/md_yellow_500" />
</RelativeLayout>
To show Fragments inside another Fragment, use getChildFragmentManager() instead of getFragmentManager()
final ImagesPagerAdapter imagesAdapter = new
ImagesPagerAdapter(getChildFragmentManager(), imageFragments);
viewPager.setAdapter(imagesAdapter);
It may not be obvious at first sight but don't try to cache or prepare fragments for a fragment adapter. Fragment(State)PagerAdapter.getItem has to return a new item and when the method is called is managed internally by the adapter.
Let the fragment adapter have data it needs to construct fragments at the right time:
final List<UserImage> userImages = user.getImages();
final ImagesPagerAdapter imagesAdapter = new ImagesPagerAdapter(getFragmentManager(), userImages);
viewPager.setAdapter(imagesAdapter);
And implement the fragment adapter so that getItem returns a new fragment:
public class ImagesPagerAdapter extends FragmentStatePagerAdapter {
#NonNull
private List<UserImage> userImages;
public ImagesPagerAdapter(#NonNull final FragmentManager fragmentManager,
#NonNull final List<UserImage> userImages) {
super(fragmentManager);
this.userImages = userImages;
}
#Override
public Fragment getItem(final int position) {
final UserImage userImage = userImages.get(position);
final SizeImage processed = userImage.getProcessed();
return UserImageFragment.newInstance(processed.getFullsize())
}
#Override
public int getCount() {
return userImages.size();
}
}
Here I see two options for improvement:
Replace List<UserImage> with List<SizeImage> or List<whatever getFullSize returns>. The less implementation details you leak to the adapter the better.
If userImage.getProcessed().getFullsize() takes time (accesses disk, performs computation) do that asynchronously in one of UserImageFragment lifecycle methods - defer to when it's needed and don't block UI thread.
Neither FragmentPagerAdapter nor FragmentStatePagerAdapter are built with updates in mind so
Don't try to modify the userImages list.
If the images change replace and reattach the whole adapter.

notifyDataSetChanged() forces scroll up in FragmentStatePagerAdapter

I have a FragmentStatePagerAdapter which is being refreshed once every second with new data for some of his pages.
The problem is that some pages has a lot of content and a vertical scroll, so every second when notifyDataSetChanged() is being called, the scroll is forzed to his upper possition. it is a very abnormal and annoying behaviour.
I find this on stackoverflow: notifyDataSetChanged() makes the list refresh and scroll jumps back to the top
The problem is that these solutions are designed for a normal ViewPager or normal Adapter and can't work with FragmentStatePageAdapter or something, because after trying them i still have the same problem.
This is my adapter:
public class CollectionPagerAdapter extends FragmentStatePagerAdapter {
public CollectionPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
Fragment fragment = new ObjectFragment();
Bundle args = new Bundle();
args.putString(ObjectFragment.ARG_TEXT, children[i]);
fragment.setArguments(args);
return fragment;
}
#Override
public int getCount() {
return infoTitlesArray.length;
}
#Override
public CharSequence getPageTitle(int position) {
return infoTitlesArray[position];
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
The ViewPager which has the problem:
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.PagerTitleStrip
android:id="#+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="4dp"
android:paddingBottom="4dp"
style="#style/CustomPagerTitleStrip"/>
</android.support.v4.view.ViewPager>
Layout of the fragment:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarSize="5dip"
style="#style/CustomScrollBar">
<TextView
android:id="#+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="left"
android:textSize="16sp"
android:padding="5dp"
style="#style/CustomTextView"/>
</ScrollView>
The java code for the fragment:
public static class ObjectFragment extends Fragment {
public static final String ARG_TEXT = "object";
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_collection_object, container, false);
Bundle args = getArguments();
TextView tv = ((TextView) rootView.findViewById(R.id.text));
tv.setText(Html.fromHtml(args.getString(ARG_TEXT)));
return rootView;
}
}
EDIT:
I've created a demo project. Here are some important pieces.
Use a FragmentStatePagerAdapter subclass.
We need a FragmentStatePagerAdapter base class in order to save the state of the fragment.
Save the scroll position of the ScrollView in onSaveInstanceState(), and set the scroll position to the saved value when the fragment view is (re)created.
Now that we are saving/restoring the fragment state, we put the scroll position in that state:
#Override
public void onSaveInstanceState(Bundle outState) {
int scrollY = scrollView.getScrollY();
outState.putInt("scrollY", scrollY);
super.onSaveInstanceState(outState);
}
and restore it in onCreateView():
if (savedInstanceState != null) {
final int scrollY = savedInstanceState.getInt("scrollY");
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.setScrollY(scrollY);
}
});
}
Set up a listener notification system for updates.
We have an interface called DataUpdateListener that is implemented by the fragment. The activity provides register/unregister methods:
public void addDataUpdateListener(DataUpdateListener listener) {
mListenerMap.put(listener.getPage(), listener);
}
public void removeDataUpdateListener(DataUpdateListener listener) {
mListenerMap.remove(listener.getPage());
}
... and the fragment registers & unregisters with the activity:
in onCreateView():
((MainActivity) getActivity()).addDataUpdateListener(this);
also
#Override
public void onDestroyView() {
((MainActivity) getActivity()).removeDataUpdateListener(this);
super.onDestroyView();
}
then anytime the data changes, the fragments all get an update notification:
for (int i = 0; i < children.length; i++) {
notifyUpdateListener(i, children[i]);
}
Note that nowhere in the code is onNotifyDataSetChanged() called on the view pager adapter.
The demo is on GitHub at https://github.com/klarson2/View-Pager-Data-Update
This is what is causing the scrolling:
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
When you call notifyDataSetChanged(), what the ViewPager does is ask you what to do with the pages it already has.
So it will call getItemPosition to find out: where should this page go? You have three options to respond:
Return an index. So if you return 2 for page 0, then the ViewPager will move the page at 0 to 2.
Return POSITION_UNCHANGED. The page will stay exactly where it is now.
Return POSITION_NONE. This means the page should no longer be displayed.
Once the ViewPager knows where the pages are moved to, it will call getItem() for any gaps in the pages.
So if you don't want the scroll to be disturbed, tell the ViewPager where to put the page instead of telling it to get rid of it and create a new one.

List Fragment inside FragmentTabHost not updating when adapter changed

I have a FragmentActivity that contains a Fragment.
this fragment contains a FragmentTabHost.
The tab host contains three tabs that each one is a ListFragment.
The lists are configured with custom adapter that getting updates from the main FragmentActivity, and calling notifyDatasetChanged on update.
My problem is that it seems the ListFragment UI never updates the view when it's shown.
If I change tabs - I can see the list updates, but I can't see the updates on real time when a list is shown on the screen.
That's the definition of the Fragment containing the FragmentTabHost:
public class SubOptionFragmentManager extends Fragment
{
FragmentTabHost mTabHost;
SoiListFragment mFrag1;
SoiListFragment mFrag2;
SoiListFragment mFrag3;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.inflate(R.layout.soi_container, container, false);
}
public void init(FragmentManager fm, int optionInstanceID)
{
mOptionInstanceID = optionInstanceID;
// find and setup the tabhost
mTabHost = (FragmentTabHost)getActivity().findViewById(android.R.id.tabhost);
mTabHost.setup(getActivity(), getChildFragmentManager(), R.id.realtabcontent);
// add the three tabs
mTabHost.addTab(mTabHost.newTabSpec("1")
.setIndicator("1"),
SoiListFragment.class,
getTabBundle("1", optionInstanceID));
mTabHost.addTab(mTabHost.newTabSpec(dsTag)
.setIndicator("2"),
SoiListFragment.class,
getTabBundle("2", optionInstanceID));
mTabHost.addTab(mTabHost.newTabSpec("3")
.setIndicator("3"),
SoiListFragment.class,
getTabBundle("3", optionInstanceID));
// get reference to the fragments added
mFrag1 = (SoiListFragment)getChildFragmentManager().findFragmentByTag("1");
mFrag2 = (SoiListFragment)getChildFragmentManager().findFragmentByTag("2");
mFrag3 = (SoiListFragment)getChildFragmentManager().findFragmentByTag("3");
}
public void HandleSoiItemUpdate(SubOptionInstanceItem soi, int fragID)
{
Log.i(TAG, "ItemUpdate:" + soi.toString());
updateItemRunnable ur = new updateItemRunnable(soi, int fragID);
getActivity().runOnUiThread(ur);
}
class updateItemRunnable implements Runnable
{
SubOptionInstanceItem mItem;
int mFragID;
public updateItemRunnable (SubOptionInstanceItem item, int fragID)
{
mItem = item;
}
public void run()
{
switch (mFragID)
{
// the second parameter - true means it calls notifyDatasetChanged() after updating the item
case 1:
mFrag1.getAdapter().updateItem(mItem, true);
break;
case 2:
mFrag2.getAdapter().updateItem(mItem, true);
break;
case 3:
mFrag3.getAdapter().updateItem(mItem, true);
break;
}
}
}
}
Also included in that class is the ListFragment object:
public static class SoiListFragment extends ListFragment
{
private int mOptionInstanceID;
private int mPageType;
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mOptionInstanceID = getArguments().getInt(FIELD_OPTION_INSTANCE_ID);
mPageType = getArguments().getInt(FIELD_PAGE_TYPE);
}
public SubOptionInstanceAdapter getAdapter()
{
return (SubOptionInstanceAdapter)getListAdapter();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// inflate the list view and return it
View v = inflater.inflate(R.layout.soi_list, container, false);
init();
return v;
}
public void init()
{
SubOptionInstanceAdapter mAdapter = null;
switch(mPageType)
{
case 1:
mAdapter = new SubOptionInstanceAdapter(getActivity(),
R.layout.soi_item,
Globals.getList1());
break;
case 2:
mAdapter = new SubOptionInstanceAdapter(getActivity(),
R.layout.soi_item,
Globals.getList2());
break;
case 3:
mAdapter = new SubOptionInstanceAdapter(getActivity(), R.layout.soi_item, Globals.getList3());
break;
}
setListAdapter(mAdapter);
}
The fragment layout "soi_container.xml" (simple layout containing FragmentTabHost):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#color/background" >
<android.support.v4.app.FragmentTabHost
android:id="#android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0" />
</android.support.v4.app.FragmentTabHost>
<FrameLayout
android:id="#+id/realtabcontent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
My first problem was the init() placement on the onCreateView part.
That caused the init() to occur each time a tab was loaded or changed - that's why I thought that the tab is being updated only when not visible.
Also, getting the ListFragments reference on init() in the container was wrong, as the fragments were not created yet on that point.
Finally my solution was to create the tabs on the container fragment during the OnStart event, getting the list fragment reference and initialize them with adapter in the onTabCreated event then updating them using the reference I got before.
EDIT: code sample for Ravi - getting the list fragment reference
public void onTabCreated(int pageType)
{
// if I don't already have the reference for the fragment
if (mFrag1 == null)
{
mFrag1 = (SoiListFragment)getFragmentManager().findFragmentByTag("1");
}
}

ViewPager's Fragment's view lost when ViewPager's parent Fragment hidden then shown

I've been seeing some strange behavior with my ViewPager along with my own FragmentStatePagerAdapter.
My View hierarchy goes like this:
-> (1) Fragment root view (RelativeLayout)
-> (2) ViewPager
-> (3) ViewPager's current fragment view
When the Fragment that is responsible for the Fragment root view (1) gets hidden (using .hide() in a fragment transaction) and then shown (with .show()), the fragment view that was currently showing in the ViewPager (3) becomes null, although the fragment still exists. Basically, my ViewPager becomes completely blank/transparent.
The only way I have found to fix this is to call
int current = myViewPager.getCurrentItem();
myViewPager.setAdapter(myAdapter);
myViewPager.setCurrentItem(current);
after the parent fragment is shown. This somehow triggers the views to be recreated and appear on screen. Unfortunately, this occasionally causes exceptions dealing with the pager adapter calling unregisterDataSetObserver() twice on an old observer.
Is there a better way to do this? I guess what I am asking is:
Why are my fragment views inside my ViewPager getting destroyed when the parent fragment of the ViewPager is hidden?
Update: this also happens when the application is "minimized" and then "restored" (by pressing the home action key and then returning).
Per request, here's my pager adapter class:
public class MyInfoSlidePagerAdapter extends FragmentStatePagerAdapter {
private ArrayList<MyInfo> infos = new ArrayList<MyInfo>();
public MyInfoSlidePagerAdapter (FragmentManager fm) {
super(fm);
}
public MyInfoSlidePagerAdapter (FragmentManager fm, MyInfo[] newInfos) {
super(fm);
setInfos(newInfos);
}
#Override
public int getItemPosition(Object object) {
int position = infos.indexOf(((MyInfoDetailsFragment)object).getMyInfo());
return position > 0 ? position : POSITION_NONE;
}
#Override
public CharSequence getPageTitle(int position) {
return infos.get(position).getName();
}
#Override
public Fragment getItem(int i) {
return infos.size() > 0 ? MyInfoDetailsFragment.getNewInstance(infos.get(i)) : null;
}
#Override
public int getCount() {
return infos.size();
}
public Location getMyInfoAtPosition(int i) {
return infos.get(i);
}
public void setInfos(MyInfo[] newInfos) {
infos = new ArrayList<MyInfo>(Arrays.asList(newInfos));
}
public int getPositionOfMyInfo(MyInfo info) {
return infos.indexOf(info);
}
}
I've renamed some variables but other than that it is exactly what I have.
You're not providing enough info for your specific issue, so I built a sample project that tries to reproduce your issue: the app has an activity that holds a fragment (PagerFragment) within a relative layout and below this layout I have a button that hides & shows above PagerFragment. PagerFragment has a ViewPager and each fragment within pager adapter simply displays a label - this fragment is named DataFragment. The label list is created in parent activity and passed to PagerFragment and then through its adapter to each DataFragment. Changing the PagerFragment visibility is done with no issues and each time it's becoming visible again it shows the previous shown label.
The key of the issue:
Use Fragment#getChildFragmentManager() when you're creating the viewpager adapter and not getFragmentManager!
Maybe you can compare this simple project with what you have and check where are the differences. So here goes (top-down):
PagerActivity (the only activity in the project):
public class PagerActivity extends FragmentActivity {
private static final String PAGER_TAG = "PagerActivity.PAGER_TAG";
#Override
protected void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
setContentView(R.layout.pager_activity);
if (savedInstance == null) {
PagerFragment frag = PagerFragment.newInstance(buildPagerData());
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().add(R.id.layout_fragments, frag, PAGER_TAG).commit();
}
findViewById(R.id.btnFragments).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
changeFragmentVisibility();
}
});
}
private List<String> buildPagerData() {
ArrayList<String> pagerData = new ArrayList<String>();
pagerData.add("Robert de Niro");
pagerData.add("John Smith");
pagerData.add("Valerie Irons");
pagerData.add("Metallica");
pagerData.add("Rammstein");
pagerData.add("Zinedine Zidane");
pagerData.add("Ronaldo da Lima");
return pagerData;
}
protected void changeFragmentVisibility() {
Fragment frag = getSupportFragmentManager().findFragmentByTag(PAGER_TAG);
if (frag == null) {
Toast.makeText(this, "No PAGER fragment found", Toast.LENGTH_SHORT).show();
return;
}
boolean visible = frag.isVisible();
Log.d("APSampler", "Pager fragment visibility: " + visible);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (visible) {
ft.hide(frag);
} else {
ft.show(frag);
}
ft.commit();
getSupportFragmentManager().executePendingTransactions();
}
}
its layout file pager_activity.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp" >
<Button
android:id="#+id/btnFragments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:text="Hide/Show fragments" />
<RelativeLayout
android:id="#+id/layout_fragments"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/btnFragments"
android:layout_marginBottom="4dp" >
</RelativeLayout>
</RelativeLayout>
Observe that I am adding the PagerFragment when the activity is first shown - and the PagerFragment class:
public class PagerFragment extends Fragment {
private static final String DATA_ARGS_KEY = "PagerFragment.DATA_ARGS_KEY";
private List<String> data;
private ViewPager pagerData;
public static PagerFragment newInstance(List<String> data) {
PagerFragment pagerFragment = new PagerFragment();
Bundle args = new Bundle();
ArrayList<String> argsValue = new ArrayList<String>(data);
args.putStringArrayList(DATA_ARGS_KEY, argsValue);
pagerFragment.setArguments(args);
return pagerFragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
data = getArguments().getStringArrayList(DATA_ARGS_KEY);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.pager_fragment, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
pagerData = (ViewPager) view.findViewById(R.id.pager_data);
setupPagerData();
}
private void setupPagerData() {
PagerAdapter adapter = new LocalPagerAdapter(getChildFragmentManager(), data);
pagerData.setAdapter(adapter);
}
}
its layout (only the ViewPager that takes full size):
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/pager_data"
android:layout_width="match_parent"
android:layout_height="match_parent" />
and its adapter:
public class LocalPagerAdapter extends FragmentStatePagerAdapter {
private List<String> pagerData;
public LocalPagerAdapter(FragmentManager fm, List<String> pagerData) {
super(fm);
this.pagerData = pagerData;
}
#Override
public Fragment getItem(int position) {
return DataFragment.newInstance(pagerData.get(position));
}
#Override
public int getCount() {
return pagerData.size();
}
}
This adapter creates a DataFragment for each page:
public class DataFragment extends Fragment {
private static final String DATA_ARG_KEY = "DataFragment.DATA_ARG_KEY";
private String localData;
public static DataFragment newInstance(String data) {
DataFragment df = new DataFragment();
Bundle args = new Bundle();
args.putString(DATA_ARG_KEY, data);
df.setArguments(args);
return df;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
localData = getArguments().getString(DATA_ARG_KEY);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.data_fragment, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
view.findViewById(R.id.btn_page_action).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(getActivity(), localData, Toast.LENGTH_SHORT).show();
}
});
((TextView) view.findViewById(R.id.txt_label)).setText(localData);
}
}
and DataFragment's layout:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="4dp" >
<Button
android:id="#+id/btn_page_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:text="Interogate" />
<TextView
android:id="#+id/txt_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textAppearance="?android:attr/textAppearanceLarge" />
</RelativeLayout>
Enjoy coding!
maybe it will help mViewPager.setOffscreenPageLimit(5)
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.
View Pager is pretty adamant in keeping keeping its Fragments fresh always and thus optimizing the performance by freeing up memory when a fragment is not used. Clearly that is a valid useful trait in a mobile system. But due to this persistent deallocation of resources the fragment is created everytime it gains focus.
mViewPager.setOffscreenPageLimit(NUMBEROFFRAGMENTSCREENS);
Here is the documentation.
this Old Post has an interesting Solution for your problem.. Please Refer
For me i changed to getChildFragmentManager() instead of getFragmentManager()
and works good.
Ex:
pagerAdapt = new PagerAdapt(getChildFragmentManager());
I had the same problem. My app (FragmentActivity) has a pager (ViewPager) with 3 framgents. While swiping between the fragments they are destroyed and recreated all the time. Actually it makes no problem in functionality (expect unclosed Cursors), but I was also wondering about this question.
I do not know if there is a workaround to change the behavior of the ViewPager, but I suggest to have a configuration object (maybe a static on) and before destroy save your myViewPager object at the config object.
public class App extends FragmentActivity {
static MyData data;
#Override
protected void onCreate(Bundle savedInstanceState) {
data = (MyData) getLastCustomNonConfigurationInstance();
if (data == null) {
data = new MyData();
data.savedViewPager = myViewPager;
} else {
myViewPager = data.savedViewPager;
}
}
#Override
public Object onRetainCustomNonConfigurationInstance() {
Log.d("onRetainCustomNonConfigurationInstance", "Configuration call");
return data;
}
}
public class MyData {
public ViewPager savedViewPager;
}
With this way, you can save the reference to the an object which won't be destroyed hence there is reference to it and you can reload all your crucial objects.
I hope you find my suggestion useful!

Updating content within fragment

I am working on an android project and I am trying to work out how I can use fragments to make a tablet friendly UI for my app. But I am unsure how to update fragment B depending on what happens in fragment A. I know I need some sort of interface but I can't work out how to implement it.
Basically, what I have is an activity called MainActivity which sets the layout for the fragments.
In landscape mode the XML file is.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="com.BoardiesITSolutions.FragmentTest.FragmentA"
android:id="#+id/list"
android:layout_weight="1"
android:layout_width="0px"
android:layout_height="match_parent">
</fragment>
<FrameLayout android:id="#+id/viewer"
android:layout_weight="1"
android:layout_width="0px"
android:layout_height="match_parent"
android:background="?android:attr/detailsElementBackground">
</FrameLayout>
</LinearLayout>
In portrait mode its
Currently the MainActivity just sets the content view to the XML file above using SetContentView within in the onCreate method. Below is how it looks.
In the FragmentA class file it extends ListFragment and contains a ListView of items and what I want to be able to do is to update the textview within Fragment B based on what is selected in Fragment A.
Below is the code for fragment A.
#Override
public View onCreateView(LayoutInflater inflator, ViewGroup container, Bundle savedInstanceState)
{
return inflator.inflate(R.layout.fragment_a, container, false);
}
#Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
myListView = getListView();
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("Item1");
arrayList.add("Item2");
arrayList.add("Item3");
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(getActivity().getApplicationContext(),
android.R.layout.simple_list_item_activated_1, arrayList);
setListAdapter(arrayAdapter);
View fragmentB = getActivity().findViewById(R.id.viewer);
mDualPane = fragmentB != null && fragmentB.getVisibility() == View.VISIBLE;
if (savedInstanceState != null)
{
mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
}
if (mDualPane)
{
myListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
showDetails(mCurCheckPosition);
}
}
#Override
public void onListItemClick(ListView l, View view, int position, long id)
{
showDetails(position);
}
private void showDetails(int index)
{
mCurCheckPosition = index;
if (mDualPane)
{
myListView.setItemChecked(index, true);
FragmentB details = (FragmentB)getFragmentManager().findFragmentById(R.id.viewer);
if (details == null || details.getShownIndex() != index)
{
details = FragmentB.newInstance(index);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.viewer, details);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
fragmentTransaction.commit();
}
}
else
{
Intent intent = new Intent(getActivity(), FragmentBActivity.class);
intent.putExtra("index", index);
startActivity(intent);
}
}
#Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("curChoice", mCurCheckPosition);
}
FragmentB contains the following code, this class extends Fragment
public View onCreateView(LayoutInflater inflator, ViewGroup container, Bundle savedInstanceState)
{
if (container == null)
{
return null;
}
View view = inflator.inflate(R.layout.fragment_b, container, false);
return view;
}
public static FragmentB newInstance(int index)
{
FragmentB fragmentB = new FragmentB();
Bundle args = new Bundle();
args.putInt("index", index);
//rgs.putString("content", content);
fragmentB.setArguments(args);
return fragmentB;
}
public int getShownIndex()
{
return getArguments().getInt("index", 0);
}
And in the Activity file for FragmentB it contains the following
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
{
finish();
return;
}
if (savedInstanceState == null)
{
FragmentB fragmentB = new FragmentB();
fragmentB.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(android.R.id.content, fragmentB).commit();
}
}
As you can see from the screenshot above, I have the basis of the fragments working and when I click on each item, it shows what the currently selected item is, but I have no idea how to tell it to update the textview in fragment b based on what the user clicked from fragment a and how this is handled in both portrait and landscape mode.
Thanks for any help you can provide.
You may override the onActivityCreated() method of FragmentB, find view by id of that TextView, and update it.
Here's a mock:
public class FragmentB extends Fragment{
//......
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
TextView textView = getActivity().findViewById(R.id.my_textview);
textView.setText("Hello World!");
}
}
but I have no idea how to tell it to update the textview in fragment b based on what the user clicked from fragment a and how this is handled in both portrait and landscape mode.
Have Fragment A call a method on the hosting activity to let it know that the user clicked on something. The hosting activity can then either call a method on Fragment B (if that fragment exists), or start up Fragment B (if the fragment does not exist but there is room for it), or start an activity that will be responsible for displaying Fragment B (e.g., on a phone).
What I wouldn't do is what you are doing: having Fragment A create Fragment B. Fragment A should not care if Fragment B exists or not; that is the activity's job. Fragment A should only worry about Fragment A, plus passing necessary events to the activity.

Categories

Resources