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.
Related
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
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?
I have been following the tutorial for fragments from Google.
I tried adding a button to the news_articles.xml layout. The problem is that this button does not disappear like ListView, when the article_view.xml is called. When I run the app, it displays the ListView along with the test button. After clicking on one of the news headlines, the description of this news is displayed along with the button.
What do I need to modify, so that the button (or any other element) will not be shown? This sample image shows the button which remains seen after clicking a news item.
Code for news_articles.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="#+id/listView1"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
</ListView>
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</FrameLayout>
Code for article_view.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="#+id/article"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:textSize="18sp" />
</FrameLayout>
MainActivity code:
public class MainActivity extends FragmentActivity
implements HeadlinesFragment.OnHeadlineSelectedListener {
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
// Check whether the activity is using the layout version with
// the fragment_container FrameLayout. If so, we must add the first fragment
if (findViewById(R.id.fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of ExampleFragment
HeadlinesFragment firstFragment = new HeadlinesFragment();
// In case this activity was started with special instructions from an Intent,
// pass the Intent's extras to the fragment as arguments
firstFragment.setArguments(getIntent().getExtras());
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
}
public void onArticleSelected(int position) {
// The user selected the headline of an article from the HeadlinesFragment
// Capture the article fragment from the activity layout
ArticleFragment articleFrag = (ArticleFragment)
getSupportFragmentManager().findFragmentById(R.id.article_fragment);
if (articleFrag != null) {
// If article frag is available, we're in two-pane layout...
// Call a method in the ArticleFragment to update its content
articleFrag.updateArticleView(position);
} else {
// If the frag is not available, we're in the one-pane layout and must swap frags...
// Create fragment and give it an argument for the selected article
ArticleFragment newFragment = new ArticleFragment();
Bundle args = new Bundle();
args.putInt(ArticleFragment.ARG_POSITION, position);
newFragment.setArguments(args);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack so the user can navigate back
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
}
}
HeadLines code:
public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener mCallback;
// The container Activity must implement this interface so the frag can deliver messages
public interface OnHeadlineSelectedListener {
/** Called by HeadlinesFragment when a list item is selected */
public void onArticleSelected(int position);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We need to use a different list item layout for devices older than Honeycomb
int layout = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ?
android.R.layout.simple_list_item_activated_1 : android.R.layout.simple_list_item_1;
ListView listViewTest=(ListView)getActivity().findViewById(R.id.listView1);
ArrayList<String> your_array_list = new ArrayList<String>();
your_array_list.add("Test1");
your_array_list.add("Test2");
ArrayAdapter<String> arrayAdapter =
new ArrayAdapter<String>(getActivity(),android.R.layout.simple_list_item_1, your_array_list);
setListAdapter(arrayAdapter);
// Create an array adapter for the list view, using the Ipsum headlines array
//setListAdapter(new ArrayAdapter<String>(getActivity(), layout, Ipsum.Headlines));
}
#Override
public void onStart() {
super.onStart();
// When in two-pane layout, set the listview to highlight the selected list item
// (We do this during onStart because at the point the listview is available.)
if (getFragmentManager().findFragmentById(R.id.article_fragment) != null) {
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception.
try {
mCallback = (OnHeadlineSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnHeadlineSelectedListener");
}
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
// Notify the parent activity of selected item
mCallback.onArticleSelected(position);
// Set the item as checked to be highlighted when in two-pane layout
getListView().setItemChecked(position, true);
}
}
ArticleFragment code:
public class ArticleFragment extends Fragment {
final static String ARG_POSITION = "position";
int mCurrentPosition = -1;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// If activity recreated (such as from screen rotate), restore
// the previous article selection set by onSaveInstanceState().
// This is primarily necessary when in the two-pane layout.
if (savedInstanceState != null) {
mCurrentPosition = savedInstanceState.getInt(ARG_POSITION);
}
// Inflate the layout for this fragment
return inflater.inflate(R.layout.article_view, container, false);
}
#Override
public void onStart() {
super.onStart();
// During startup, check if there are arguments passed to the fragment.
// onStart is a good place to do this because the layout has already been
// applied to the fragment at this point so we can safely call the method
// below that sets the article text.
Bundle args = getArguments();
if (args != null) {
// Set article based on argument passed in
updateArticleView(args.getInt(ARG_POSITION));
} else if (mCurrentPosition != -1) {
// Set article based on saved instance state defined during onCreateView
updateArticleView(mCurrentPosition);
}
}
public void updateArticleView(int position) {
TextView article = (TextView) getActivity().findViewById(R.id.article);
article.setText(Ipsum.Articles[position]);
mCurrentPosition = position;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save the current article selection in case we need to recreate the fragment
outState.putInt(ARG_POSITION, mCurrentPosition);
}
}
Create a new xml file that will be used in MainActivity - for example activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</RelativeLayout>
In MainActivity modify the code, so that it uses the new xml layout -
setContentView(R.layout.activity_main);
Further modify the onCreateMethod so it looks like this
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
return;
}
HeadlinesFragment firstFragment = new HeadlinesFragment();
firstFragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
In HeadLinesFragment override the onCreateView method
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.news_articles, container, false);
return rootView;
}
That should be all. Try it out by adding some sample buttons in the news_articles.xml.
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)
}
}
public class MainActivity extends Activity implements MainMenuFragment.OnMainMenuItemSelectedListener {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
// add menu fragment
MainMenuFragment myFragment = new MainMenuFragment();
fragmentTransaction.add(R.id.menu_fragment, myFragment);
//add content
DetailPart1 content1= new DetailPart1 ();
fragmentTransaction.add(R.id.content_fragment, content1);
fragmentTransaction.commit();
}
public void onMainMenuSelected(String tag) {
//next menu is selected replace existing fragment
}
I have a need to display two list views side by side, menu on left and its content on right side. By default, the first menu is selected and its content is displayed on right side. The Fragment that displays content is as below:
public class DetailPart1 extends Fragment {
ArrayList<HashMap<String, String>> myList = new ArrayList<HashMap<String, String>>();
ListAdapter adap;
ListView listview;
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(savedInstanceState!=null){
myList = (ArrayList)savedInstanceState.getSerializable("MYLIST_obj");
adap = new LoadImageFromArrayListAdapter(getActivity(),myList );
listview.setAdapter(adap);
}else{
//get list and load in list view
getlistTask = new GetALLListTasks().execute();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.skyview_fragment, container,false);
return v;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("MYLIST_obj", myList );
}
}
The onActivityCreated and onCreateView are called twice. There are many examples out there using fragments. Since I am beginner in this subject, I am unable relate the example with my problem. I need a fool proof way to handle orientation change. I have NOT declared android:configChanges in manifest file. I need the activity destroy and recreate so that I can use different layout in landscape mode.
You are creating a new fragment every time you turn the screen in your activity onCreate(); But you are also maintaining the old ones with super.onCreate(savedInstanceState);. So maybe set tag and find the fragment if it exists, or pass null bundle to super.
This took me a while to learn and it can really be a pain when you are working with stuff like viewpager.
I'd recommend you to read about fragments an extra time as this exact topic is covered.
Here is an example of how to handle fragments on a regular orientation change:
Activity:
public class MainActivity extends FragmentActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
TestFragment test = new TestFragment();
test.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, test, "your_fragment_tag").commit();
} else {
TestFragment test = (TestFragment) getSupportFragmentManager().findFragmentByTag("your_fragment_tag");
}
}
}
Fragment:
public class TestFragment extends Fragment {
public static final String KEY_ITEM = "unique_key";
public static final String KEY_INDEX = "index_key";
private String mTime;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
if (savedInstanceState != null) {
// Restore last state
mTime = savedInstanceState.getString("time_key");
} else {
mTime = "" + Calendar.getInstance().getTimeInMillis();
}
TextView title = (TextView) view.findViewById(R.id.fragment_test);
title.setText(mTime);
return view;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("time_key", mTime);
}
}
A good guideline about how to retain data between orientation changes and activity recreation can be found in android guidelines.
Summary:
make your fragment retainable:
setRetainInstance(true);
Create a new fragment only if necessary (or at least take data from it)
dataFragment = (DataFragment) fm.findFragmentByTag("data");
// create the fragment and data the first time
if (dataFragment == null) {