I have an activity and with a button I am switching between the two fragments (MAIN & SETTINGS). In the MAIN fragment I have a ViewPager with 4 child fragments.
At first run everything works fine, but if I rotate the screen, the getActivity() for fragments within the ViewPager is returning null.
ActivityMain:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Add or show the fragments
showHideScreenFragment(FRAGMENT_MAIN);
}
private void showHideScreenFragment(String tag) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
// Get the fragment from the backstack if it is existing
BaseFragment oldFragment = getFragmentFromBackstack(tag);
// Get the current fragment from the layout
BaseFragment currentFragment = getCurrentFragment();
if (oldFragment == null) {
if (currentFragment != null) {
ft.hide(currentFragment);
}
ft.add(getMainContainerId(), getFragmentInstance(tag), tag);
}
else {
if (currentFragment != null) {
if (isSameFragment(oldFragment, currentFragment))
return;
ft.hide(currentFragment);
}
if (oldFragment.isHidden())
ft.show(oldFragment);
}
ft.commit();
fm.executePendingTransactions();
}
private BaseFragment getFragmentInstance(String tag) {
if (tag.equals(FRAGMENT_MAIN)) return getFragmentMain();
if (tag.equals(FRAGMENT_SETTINGS)) return getFragmentSettings();
throw new RuntimeException("Fragment not found !");
}
private FragmentMain getFragmentMain() {
return new FragmentMain();
}
private FragmentSettings getFragmentSettings() {
return new FragmentSettings();
}
private BaseFragment getFragmentFromBackstack(String tag) {
if (tag.equals(FRAGMENT_MAIN)) return getFragmentMainFromBackstack();
if (tag.equals(FRAGMENT_SETTINGS)) return getFragmentSettingsFromBackstack();
throw new RuntimeException("Fragment not found !");
}
private FragmentMain getFragmentMainFromBackstack() {
return (FragmentMain) getSupportFragmentManager().findFragmentByTag(FRAGMENT_MAIN);
}
private FragmentSettings getFragmentSettingsFromBackstack() {
return (FragmentSettings) getSupportFragmentManager().findFragmentByTag(FRAGMENT_SETTINGS);
}
private boolean isSameFragment(Fragment f1, Fragment f2) {
return f1.getTag().equals(f2.getTag());
}
FragmentMain:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
viewPager = (ViewPager) view.findViewById(R.id.viewPager);
// Add the 4 child fragments to the viewpager
populateViewPager();
// Debugging
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
_printFragmentStates();
}
}, 2500);
return view;
}
private void populateViewPager() {
ArrayList<BaseMainFragment> fragments = new ArrayList<BaseMainFragment>();
fragments.add(new FragmentSearch());
fragments.add(new FragmentFavorites());
fragments.add(new FragmentHouse());
fragments.add(new FragmentRoom());
adapterMain = new AdapterMain(getChildFragmentManager(), fragments);
viewPager.setOffscreenPageLimit(4);
viewPager.setAdapter(adapterMain);
}
// DEBUGGING
private void _printFragmentStates() {
Activity actSearch = null;
Activity actFav = null;
Activity actHouse = null;
Activity actRoom = null;
actSearch = getFragmentSearch().getActivity();
actFav = getFragmentFavorites().getActivity();
actHouse = getFragmentHouse().getActivity();
actRoom = getFragmentRoom().getActivity();
Functions.logd("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
Functions.logd("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
Functions.logd("Main fragment act, is null: " + (getActivity() == null));
Functions.logd("Search act, is null: " + (actSearch == null));
Functions.logd("Favorite act, is null: " + (actFav == null));
Functions.logd("House act, is null: " + (actHouse == null));
Functions.logd("Room act, is null: " + (actRoom == null));
Functions.logd("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
Functions.logd("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
}
private FragmentSearch getFragmentSearch() {
return (FragmentSearch) adapterMain.getItem(0);
}
private FragmentFavorite getFragmentFavorite() {
return (FragmentFavorite) adapterMain.getItem(1);
}
private FragmentHouse getFragmentHouse() {
return (FragmentHouse) adapterMain.getItem(2);
}
private FragmentRoom getFragmentHouse() {
return (FragmentRoom) adapterMain.getItem(3);
}
As I said, at first run everything works fine, but after I rotate the screen, I am getting null for getActivity(); in the 4 child fragments: Search, Favorite, House and Room.
Logcat debug
1 run:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Main fragment act, is null: false
Search act, is null: false
Favorite act, is null: false
House act, is null: false
Room act, is null: false
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
After screen orientation changed:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Main fragment act, is null: false
Search act, is null: true
Favorite act, is null: true
House act, is null: true
Room act, is null: true
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
What am I doing wrong?
After hours of debugging, I figured out that if you're having only 1 fragment (without child or nested fragments) attached to your activity, then you don't need to re-add your fragment.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Add or show the fragments if the savedInstance is null, otherwise let the system reattach your fragment.
if (savedIstance == null)
showHideScreenFragment(FRAGMENT_MAIN);
}
You don't need to reattach the fragment, the android system will do this for you.
And the solution for getting NPE at getActivity(); in child fragments is:
Use FragmentStatePagerAdapter for your ViewPager's adapter.
and override the saved state method:
#Override
public Parcelable saveState() {
return null;
}
I don't know why, but setRetainInstance(false); does not helped me, and I think this will remain a mystery for me.
It's a good practice to follow a pattern like this when working with fragments :
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view = view.findViewById(R.id.view);
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// do your thing here
populateViewPager();
}
1) Use OnCreateView to set your fragment's view,
2) OnViewCreated to initialize view and
3) OnActivityCreated to setup things.
Using getActivity() inside OnActivityCreated ensures that getActivity() does not return null. This method gets called only after the activity is fully initialized. OnAttach, OnCreate, OnCreateView may get even before the activity is created.
Related
I use mAdapter.getTotalPriceInRecyclerView() to get total price in current selected page in viewpage2+Tablayout.
but it will cause NullPointerException because the mAdapter created in onCreateView method.
How could I make sure mAdapter has been initialed?
I use viewpage2 to create new Fragment (TheFragmentClass.newInstance()) rather than beginTransaction().commit
private boolean createFragment(int tabLimited) {
if (mVp2Adapter.getItemCount() >= tabLimited) {
return false;
}
String tabText = getTimeOfHMS();
SettlementProductItemFragment fragment = SettlementProductItemFragment.newInstance(); // new intance
mVp2Adapter.addFragment(tabText, fragment); // add to viewpage2's adapter
mTabSettlementProduct.selectTab(mTabSettlementProduct.getTabAt(
mVp2Adapter.getItemCount() - 1));
return true;
}
public void addFragment(String title, Fragment fragment) {
if (mFragmentTitles.contains(title)) {
Log.e(TAG, "addFragment failed: mFragmentTitles.contains(" + title + ")");
return;
}
mFragments.put(title, fragment);
mFragmentTitles.add(title);
updateHashMap();
notifyItemInserted(mFragmentTitles.size() - 1);
}
Here's my SettlementProductItemFragment class.
public class SettlementProductItemFragment extends Fragment {
private RecyclerView mRvProductInFragment;
public SettlementProductItemFragment() {
}
public static SettlementProductItemFragment newInstance() {
SettlementProductItemFragment fragment = new SettlementProductItemFragment();
Bundle args = new Bundle();
fragment.setArguments(args);
return fragment;
}
private SettlementProductItemRecyclerViewAdapter mAdapter;
public double getTotalPriceInFragment() {
if (mAdapter == null) {
throw new NullPointerException("mAdapter(SettlementProductItemRecyclerViewAdapter) CAN NOT BE NULL");
}
return mAdapter.getTotalPriceInRecyclerView();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_settlement_product_item_list, container, false);
Log.e("getTotalPriceInFragment", "onCreateView: " + view.getClass().toString());
// Set the adapter
if (view instanceof RecyclerView) {
Context context = view.getContext();
mAdapter = new SettlementProductItemRecyclerViewAdapter(context, getProducts());
mRvProductInFragment = (RecyclerView) view;
mRvProductInFragment.setLayoutManager(new LinearLayoutManager(context));
mRvProductInFragment.setAdapter(mAdapter);
}
return view;
}
}
Since you use ViewPager2 you have setOffscreenPageLimit method (offscreenPageLimit property in Kotlin) it will retain (and precreate also) your fragment when you initilize your ViewPager2.
The problem that i see in your code is that you modify items in ViewPager adapter. It isn't default snippet for using viewPager2 + tabs, so make sure you do it well, check for ViewPager2 samples by Google
Alternatively, you can create property in your SettlementProductItemFragment e.g isInitialized and observe it in your host fragment. In that way i suppose you have to use Architecture Components like ViewModel + Livedata
I'm passing value from adapter to fragment class,
Here adapter class,
rbFolder.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
if(selected != null)
{
selected.setChecked(false);
String meee = data.get(position).getTypeId();
System.out.println("*******so*inside********"+meee);
}
rbFolder.setChecked(true);
String so = data.get(position).getTypeId();
System.out.println("*******so*********"+so);
selected = rbFolder;
System.out.println("********selected*******"+selected);
Fragment homepage = new Fragment();
FragmentTransaction fragmentManager =((FragmentActivity)context).getSupportFragmentManager()
.beginTransaction();
Bundle bundle=new Bundle();
bundle.putString("name", so); //key and value
System.out.println("*****venki***meee*******"+so);
homepage.setArguments(bundle);
// fragmentManager.replace(R.id.content_frame, homepage);
fragmentManager.addToBackStack(null);
fragmentManager.commit();
}
});
In this, putstring working well but in my fragemt class i didnt receive values. I'm trouble in this place.
Here Fragmnet class,
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (rootview != null) {
ViewGroup parent = (ViewGroup) rootview.getParent();
if (parent != null)
parent.removeView(rootview);
}
try {
rootview = inflater.inflate(R.layout.homepage, container, false);
} catch (InflateException e) {
/* map is already there, just return view as it is */
}
Here I get values from adapter but this is not working,
Bundle bundle = ((Activity)context).getIntent().getExtras();
if (bundle != null)
{
String strtext=getArguments().getString("name");
System.out.println("*******strtext*********"+strtext);
}
return rootview;
}
First of All you have to call your HomeFragment() instead of fragment like that
Fragment homepage = new HomeFragment();
FragmentTransaction fragmentManager =((FragmentActivity)context).getSupportFragmentManager()
.beginTransaction();
Bundle bundle=new Bundle();
bundle.putString("name", so); //key and value
homepage.setArguments(bundle);
fragmentManager.replace(R.id.content_frame, homepage);
fragmentManager.addToBackStack(null);
fragmentManager.commit();
When you want to get value inside home fragment then
if( getArguments() != null)
String strtext = getArguments().getString("name");
follow steps one by one, copy Context context;
List rowItems;
onItemClickListner onItemClickListner; and pest into your adapter
then copy two constructor methods and then pest on item click method to pass data to fragment.
in your fragment class set adapter item on click listener code in this way...
madapter.setOnClickListener(new MyListAdapterTemp.onItemClickListner() {
#Override
public void onClick(String str) {
}
});
Context context;
List<String> rowItems;
onItemClickListner onItemClickListner;
public MyListAdapterTemp(Context context, List<String> items) {
this.context = context;
this.rowItems = items;
}
public void setOnClickListener(onItemClickListner onItemClickListner) {
this.onItemClickListner = onItemClickListner;
}
holder.txtDesc.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onItemClickListner.onClick(rowItems.get(position).toString().trim().toUpperCase());
}
});
this method works fine on my app and for activity to fragment , fragment to activity we can pass data using interface but not this
way this is best and proper way to perform this type of operations.
Fragment homepage = new Fragment();
this is supposed to be your Fragment class name -
Fragment homepage = new MyFragment();
And Your Fragment be like -
public class MyFragment extends Fragment {
#Override
public void onCreate(...) {
Bundle b = getArguments(); //To get extras from Fragment
}
}
Other than that,
Bundle bundle = ((Activity)context).getIntent().getExtras();
This is used for an Activity. For Fragment you have to use getArguments()
I created this example to understand the lifecycle of android fragments at screen orientation change.
MainActivity is a container for DrawerLayout, DrawerLayout allows you to choose a fragment, which will fill the MainActivity screen.
public class MainActivity extends ActionBarActivity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("MainActivity", "onCreate savedInstanceState = "+(savedInstanceState == null ? "null" : "not null"));
...
mDrawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (position == 0) {
if (viewingPosition == position) {
mDrawerLayout.closeDrawer(mDrawerList);
return;
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.contentFrame, new ParentPagerFragment(),
ParentPagerFragment.TAG).commit();
viewingPosition = 0;
}
if (position == 1) {
if (viewingPosition == position) {
mDrawerLayout.closeDrawer(mDrawerList);
return;
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.contentFrame, ChildTextViewFragment.newInstance("hello fragment"), ChildTextViewFragment.TAG)
.commit();
viewingPosition = 1;
}
}
...
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.contentFrame, new ParentPagerFragment(),
ParentPagerFragment.TAG).commit();
...
Then I have ParentPagerFragment, ParentPagerFragment contains only a ViewPager with 3 ChildTextViewFragments.
public class ParentPagerFragment extends Fragment {
...
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d(TAG, "onCreateView savedInstanceState is "+(savedInstanceState == null ? "null" : "not null"));
View v = inflater.inflate(R.layout.fragment_pager, container, false);
List<ChildTextViewFragment> viewFragments = new ArrayList<>();
viewFragments.add(ChildTextViewFragment.newInstance("Fragment1"));
viewFragments.add(ChildTextViewFragment.newInstance("Fragment2"));
viewFragments.add(ChildTextViewFragment.newInstance("Fragment3"));
MyPagerAdapter mPagerAdapter = new MyPagerAdapter(getChildFragmentManager(), viewFragments);
ViewPager mViewPager = (ViewPager) v.findViewById(R.id.pager);
mViewPager.setAdapter(mPagerAdapter);
return v;
}
...
}
class MyPagerAdapter extends FragmentPagerAdapter {
List<ChildTextViewFragment> viewFragments;
public MyPagerAdapter(FragmentManager fm, List<ChildTextViewFragment> viewFragments) {
super(fm);
this.viewFragments = viewFragments;
}
#Override
public Fragment getItem(int index) {
return viewFragments.get(index);
}
#Override
public int getCount() {
return viewFragments.size();
}
#Override
public CharSequence getPageTitle(int position){
if(position == 0) {
return "Fragment1";
} else if (position == 1) {
return "Fragment2";
} else {
return "Fragment3";
}
}
}
ChildTextViewFragment is used only to display some text
public class ChildTextViewFragment extends Fragment {
...
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_text, container, false);
TextView mTextView = (TextView) view.findViewById(R.id.textView1);
String text = getArguments().getString(TEXT_KEY);
mTextView.setText(text);
Log.d(TAG, text+" :: onCreateView savedInstanceState is " + (savedInstanceState == null ? "null" : "not null"));
return view;
}
After I run this example for the first time I got these logs messages as expected:
06-01 10:34:24.154: D/MainActivity(8426): onCreate savedInstanceState = null
06-01 10:34:24.272: D/ParentPagerFragment(8426): onCreateView savedInstanceState is null
06-01 10:34:24.389: D/ChildTextViewFragment(8426): Fragment1 :: onCreateView savedInstanceState is null
06-01 10:34:24.390: D/ChildTextViewFragment(8426): Fragment2 :: onCreateView savedInstanceState is null
The surprise appeared, when I rotated the display:
06-01 10:36:15.697: D/MainActivity(8426): onCreate savedInstanceState = not null
06-01 10:36:15.713: D/ParentPagerFragment(8426): onCreateView savedInstanceState is not null
06-01 10:36:15.716: D/ChildTextViewFragment(8426): Fragment1 :: onCreateView savedInstanceState is not null
06-01 10:36:15.717: D/ChildTextViewFragment(8426): Fragment2 :: onCreateView savedInstanceState is not null
06-01 10:36:15.718: D/ParentPagerFragment(8426): onCreateView savedInstanceState is null
06-01 10:36:15.739: D/ChildTextViewFragment(8426): Fragment1 :: onCreateView savedInstanceState is null
06-01 10:36:15.740: D/ChildTextViewFragment(8426): Fragment2 :: onCreateView savedInstanceState is null
I was expecting all fragments will be restored and displayed again (like the first 4 line of this logs shows), but I don't understand, why are all the fragments created again with savedInstanceState = null ? Is this a common behavior, or am I doing something wrong ?
Try adding null check for savedInstanceState in your MainActivity
onCreate(Bundle savedInstanceState) method like this
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.contentFrame, new ParentPagerFragment(),
ParentPagerFragment.TAG).commit();
}
You are using FragmentPagerAdapter . So, By default it will try to load two fragments of two indices at a time in your viewpager.
So there you need to mention the pager.offScreenPageLimit(0) to load it once.
Having a look at this thread, I have a fundamental question.
1) Imagine I have a multi-pane layout like this one:
2) Now lets imagine that the underlying xml is like this one (for simplicity's sake most attributes are missed):
somefragment_land.xml:
<LinearLayout orientation="horizontal" ...>
<!--our side menu-->
<ListView id="#+id/menu" />
<!--our details fragment container-->
<FrameLayout id="#+id/container"/>
</LinearLayout>
3) Ok, so we have this SomeFragment class:
public class SomeFragment extends Fragment {
public static final String TAG = "TAGTAGTAG";
private static final String STATE_SELECTED_POSITION = "selected_position";
private int currentSelectedPosition;
private ListView mMenu;
private MyAdapter mAdapter;
private boolean isMultipaneMode;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isMultipaneMode = getResources().getBoolean(R.bool.show_fragment_multiplane);
if (savedInstanceState != null) {
currentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, 0);
} else if (isMultipaneMode) {
currentSelectedPosition = 0;
}
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
int resId = isMultipaneMode ? R.layout.fragment_somefragment_land : R.layout.fragment_somefragment;
View root = inflater.inflate(resId, container, false);
mMenu = (ListView) root.findViewById(R.id.menu);
mMenu.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
SomeItem item = mAdapter.getItem(position);
showDetails(item);
}
});
///do some stuff creating adapter
mMenu.setAdapter(mAdapter);
if (isMultipaneMode) {
showDetails(mAdapter.getItem(currentSelectedPosition));
}
return root;
}
#Override
public void onDestroyView() {
//remove details fragment
destroyDetails();
super.onDestroyView();
}
private void destroyDetails() {
if (isMultipaneMode) {
//schedule a transaction to remove a fragment
//it will happen after SomeFragment is removed
FragmentManager fm = getFragmentManager();
Fragment fragmentByTag = fm.findFragmentByTag(FragmentDetails.TAG);
if (fragmentByTag == null) {
L.e(this.getClass(), "Details fragment removed");
return;
}
fm.beginTransaction()
.remove(fragmentByTag)
.commit();
}
}
private void showDetails(SomeItem item) {
if (isMultipaneMode) {
FragmentDetails details = new FragmentDetails();
Bundle args = new Bundle();
args.putString(FragmentDetails.ARG_ID, item.getId());
details.setArguments(args);
getFragmentManager()
.beginTransaction()
.replace(R.id.fragment, details, FragmentDetails.TAG)
.commit()
;
} else {
ActivityDetail.launch(getActivity(), item.getTitle(), item.getType());
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (isMultipaneMode) {
outState.putInt(STATE_SELECTED_POSITION, currentSelectedPosition);
}
}
}
So the logic is straightforward, show details in Fragment (for multipane mode) or start Details activity if we are running on a smartphone etc
What I want to know is - how much wrong is this approach in terms of Fragment management?
I imagine myself the following case:
SomeFragment is added to FragmentManager
user decides to go elsewhere
Transaction_1 is started to remove SomeFragment
this calls to onDestroyView() which schedules a transaction to
remove DetailsFragment
Transaction_1 is complete, however, DetailsFragment is not yet
removed. It possibly holds some part of SomeFragment view hierarchy
in memory
Transaction_2 is started to remove DetailsFragment
Transaction_2 is complete, DetailsFragment is destroyed
???
These question marks stand for some uncertainty - have I created a memory leak? Or something worse? Any off-top-of-your-head consequences of using this approach?
Problem
A Fragment is not reattached to its hosting ViewPager after returning from another fragment.
Situation
One Activity hosting a Fragment whose layout holds a ViewPager (PageListFragment in the example below). The ViewPager is populated by a FragmentStateViewPagerAdapter. The single Fragments hosted inside the pager (PageFragment in the example below) can open sub page lists, containing a new set of pages.
Behaviour
All works fine as long as the back button is not pressed. As soon as the user closes one of the sub PageLists the previous List is recreated, but without the Page that was displayed previously. Swiping through the other pages on the parent PageList still works.
Code
A sample application can be found on github:
Activity
public class MainActivity extends FragmentActivity {
private static final String CURRENT_FRAGMENT = MainActivity.class.getCanonicalName() + ".CURRENT_FRAGMENT";
public static final String ARG_PARENTS = "Parents";
public void goInto(String mHostingLevel, String mPosition) {
Fragment hostingFragment = newHostingFragment(mHostingLevel, mPosition);
addFragment(hostingFragment);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addBaseFragment();
}
private void addBaseFragment() {
Fragment hostingFragment = newHostingFragment("", "");
addFragment(hostingFragment);
}
private Fragment newHostingFragment(String mHostingLevel, String oldPosition) {
Fragment hostingFragment = new PageListFragment();
Bundle args = new Bundle();
args.putString(ARG_PARENTS, mHostingLevel + oldPosition +" > ");
hostingFragment.setArguments(args);
return hostingFragment;
}
private void addFragment(Fragment hostingFragment) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragmentSpace, hostingFragment, CURRENT_FRAGMENT);
transaction.addToBackStack(null);
transaction.commit();
}
}
PageListFragment
public class PageListFragment extends Fragment {
private String mParentString;
public PageListFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_hosting, container, false);
}
#Override
public void onResume() {
mParentString = getArguments().getString(MainActivity.ARG_PARENTS);
ViewPager viewPager = (ViewPager) getView().findViewById(R.id.viewPager);
viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getFragmentManager(),mParentString));
super.onResume();
}
private static class SimpleFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
private String mHostingLevel;
public SimpleFragmentStatePagerAdapter(FragmentManager fm, String hostingLevel) {
super(fm);
this.mHostingLevel = hostingLevel;
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
PageFragment pageFragment = new PageFragment();
Bundle args = new Bundle();
args.putString(MainActivity.ARG_PARENTS, mHostingLevel);
args.putInt(PageFragment.ARG_POSITION, position);
pageFragment.setArguments(args);
return pageFragment;
}
#Override
public int getCount() {
return 5;
}
}
}
PageFragment
public class PageFragment extends Fragment {
public static final String ARG_POSITION = "Position";
private String mHostingLevel;
private int mPosition;
public PageFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View contentView = inflater.inflate(R.layout.fragment_page, container, false);
setupTextView(contentView);
setupButton(contentView);
return contentView;
}
private void setupTextView(View contentView) {
mPosition = getArguments().getInt(ARG_POSITION);
mHostingLevel = getArguments().getString(MainActivity.ARG_PARENTS);
TextView text = (TextView) contentView.findViewById(R.id.textView);
text.setText("Parent Fragments " + mHostingLevel + " \n\nCurrent Fragment "+ mPosition);
}
private void setupButton(View contentView) {
Button button = (Button) contentView.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
openNewLevel();
}
});
}
protected void openNewLevel() {
MainActivity activity = (MainActivity) getActivity();
activity.goInto(mHostingLevel, Integer.toString(mPosition));
}
}
After a lengthy investigation it turns out to be a problem with the fragment manager.
When using a construct like the one above the fragment transaction to reattach the fragment to the page list is silently discarded. It is basically the same problem that causes a
java.lang.IllegalStateException: Recursive entry to executePendingTransactions
when trying to alter the fragments inside the FragmentPager.
The same solution, as for problems with this error, is also applicable here. When constructing the FragmentStatePagerAdapter supply the correct child fragment manager.
Instead of
viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getFragmentManager(),mParentString));
do
viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getChildFragmentManager(),mParentString));
See also: github
What Paul has failed to mention is, if you use getChildFragmentManager, then you will suffer the "blank screen on back pressed" issue.
The hierarchy in my case was:
MainActivity->MainFragment->TabLayout+ViewPager->AccountsFragment+SavingsFragment+InvestmentsFragment etc.
The problem I had was that I couldn't use childFragmentManagerfor the reason that a click on the item Account view (who resides inside one of the Fragments of the ViewPager) needed to replace MainFragment i.e. the entire screen.
Using MainFragments host Fragment i.e. passing getFragmentManager() enabled the replacing, BUT when popping the back-stack, I ended up with this screen:
This was apparent also by looking at the layout inspector where the ViewPager is empty.
Apparently looking at the restored Fragments you would notice that their View is restored but will not match the hierarchy of the popped state. In order to make the minimum impact and not force a re-creation of the Fragments I re-wrote FragmentStatePagerAdapter with the following changes:
I copied the entire code of FragmentStatePagerAdapter and changed
#NonNull
#Override
public Object instantiateItem(#NonNull ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
...
}
with
#NonNull
#Override
public Object instantiateItem(#NonNull ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.detach(f);
mCurTransaction.attach(f);
return f;
}
}
...
}
This way I am effectively making sure that that the restored Fragments are re-attached to the ViewPager.
Delete all page fragments, enabling them to be re-added later
The page fragments are not attached when you return to the viewpager screen as the FragmentStatePagerAdapter is not re-connecting them. As a work-around, delete all the fragments in the viewpager after popbackstack() is called, which will allow them to be re-added by your initial code.
[This example is written in Kotlin]
//Clear all fragments from the adapter before they are re-added.
for (i: Int in 0 until adapter.count) {
val item = childFragmentManager.findFragmentByTag("f$i")
if (item != null) {
adapter.destroyItem(container!!, i, item)
}
}