Access fragment variable outside of Fragment in view pager - android

enter image description here
Before reading the question, please refer to image.
I am using viewpager to show the fragment.
Problem
In the fragment, I have used two edittext lets say editText1, editText2 now the problem is how I will get the editText data. I can only get the editText values when user click on next button but the next button is outside of fragment. How do I access the editText outside the fragment.
Before downvoting the question, let me know the reason so that I can improve my question.
Fragment java class
// newInstance constructor for creating fragment with arguments
public static BpDetails newInstance(int page) {
BpDetails fragmentFirst = new BpDetails();
Bundle args = new Bundle();
args.putInt("someInt", page);
fragmentFirst.setArguments(args);
return fragmentFirst;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
page = getArguments().getInt("someInt", 0);
}
// Inflate the view for the fragment based on layout XML
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.bp_details, container, false);
Log.i("View ",view.toString());
Log.i("DOB is ",Long.toString(Constants.dob));
systolic =(EditText) view.findViewById(R.id.systolic);
diastolic =(EditText) view.findViewById(R.id.diastolic);
return view;
}
ViewPager Activity
vpPager = (ViewPager) findViewById(R.id.view_pager);
adapterViewPager = new MyPagerAdapter(getSupportFragmentManager());
vpPager.setAdapter(adapterViewPager);
Fragment fragment=adapterViewPager.getItem(prevPage);
if (fragment.getClass().equals(BpDetails.class)){
Log.i("Call ","Yes");
}
findViewById(R.id.btn_prev).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// checking for last page
// if last page home screen will be launched
int current = getItem(-1);
if (current!=0)
prevPage=current-1;
if (current < 4) {
// move to next screen
vpPager.setCurrentItem(current);
} else {
//final reached.
}
}
});
findViewById(R.id.btn_next).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// checking for last page
// if last page home screen will be launched
int current = getItem(+1);
if (current!=0)
prevPage=current-1;
System.out.println("Prev page "+prevPage);
if (current < 4) {
// move to next screen
Fragment prevFragment=adapterViewPager.getItem(prevPage);
} else {
//final reached.
}
}
});
}
private int getItem(int i) {
return vpPager.getCurrentItem() + i;
}
public static class MyPagerAdapter extends FragmentPagerAdapter {
private static int NUM_ITEMS = 4;
private static int mSelectedPosition;
public MyPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
//mSelectedPosition=selectedPosition;
}
// Returns total number of pages
#Override
public int getCount() {
return NUM_ITEMS;
}
// Returns the fragment to display for that page
#Override
public Fragment getItem(int position) {
switch (position) {
case 0: // Fragment # 0 - This will show FirstFragment
return BasicDetails.newInstance(0);
case 1:
return BpDetails.newInstance(1);
case 2:
return BslDetails.newInstance(2);
case 3:
return Summary.newInstance(3);
default:
return null;
}
}
}

Create two getters inside your fragment like this.
public String getSystolic(){
return this.systolic.getText().toString();
}
public String getDiastolic(){
return this.diastolic.getText().toString();
}
BpDetails fr = (BpDetails)myAdapter.getItem(myViewPager.getCurrentItem());
String systolicString = fr.getSystolic();

I had a similar issue. .getItem() instantiates a new Fragment, so upon calling myAdapter.getItem(...) you would be getting null for all elements in the Fragment, but not null for the Fragment.
When I fixed this, what I had to do was create another method inside of MyPagerAdapter called getInstantiatedFragment:
public Fragment getInstantiatedFragment(int position)
{
return fragments.get(position);
}
fragments is a new field for the class:
private ArrayList<Fragment> fragments = new ArrayList<>();
I would override getItem() (as you have done already) and change it to:
#Override
public Fragment getItem(int position)
{
switch (position) {
case 0:
BasicDetails basicDetails = BasicDetails.newInstance(0);
fragments.add(basicDetails);
return basicDetails;
...
}
where you're adding the fragment to fragments before returning, then you would call:
BpDetails fr = (BpDetails)myAdapter.getInstantiatedItem(myViewPager.getCurrentItem());
to get the instance of the created fragment and then call
String systolicString = fr.getSystolic();
if you're using the previous answer's method.
This is so that you can keep track of the instantiated fragments in fragments. I'm sure there are better ways.

Related

How to properly use ViewPager in android

I want to make a quiz application. So far I have 3 activities - home, quiz, score. Since the quiz activity contains multiple equivalent views ( image header, question and 4 answer buttons ), I did some reading and decided that
ViewPager with FragmentStatePagerAdapter show do the trick. So I made an xml template and inflated couple of test views and it was all looking good, until I started handling the user interaction.
I want to simulate a toggle button and there is only one correct answer to each question, so selecting one button should deselect the previous one ( if any ). When the button is pressed I change my Question model, then I find all 4 buttons with findViewById and reset their color filter. Then I set that filter back on my selected button. To determine which question model to update I use the current fragment position, which I have set ( using setTag, in fragment's onCreate ) in my template root view.
This is how I call my fragmets:
public Fragment getItem(int position) {
Question question = Repository.findById(position);
int correctAnswerBtnId;
switch (question.getCorrectAnswerIndex()) {
case 0: correctAnswerBtnId = R.id.quiz_answer_0_btn; break;
case 1: correctAnswerBtnId = R.id.quiz_answer_1_btn; break;
case 2: correctAnswerBtnId = R.id.quiz_answer_2_btn; break;
case 3: correctAnswerBtnId = R.id.quiz_answer_3_btn; break;
this.ACTIVITY_ROOT.setTag(question.getID());
Fragment fragment = new QuestionFragment();
Bundle args = new Bundle();
args.putSerializable(QuestionFragment.QUESTION, question);
fragment.setArguments(args);
return fragment;
}
My QuestionFragment onCreateView is as per documentation:
public View onCreateView(
LayoutInflater inflater,
ViewGroup container,
Bundle questionData) {
this.rootView = inflater.inflate(
R.layout.layout_question_template,
container,
false);
Bundle args = getArguments();
this.question = (Question) args.getSerializable(QuestionFragment.QUESTION);
populateInflatable();
rootView.findViewById(R.id.layout_question_template_root).setTag(this.question.getID());
return rootView;
}
In populateInflatable I use this.rootView to fintViewById and populate it with my question data. Then I change the color of a button, if there is selected one from the Question.
On button click I call selectAnserButton :
public void selectAnswerButton(View selectedButton) {
int questionId =
(int) this.activityRoot.findViewById(
R.id.layout_question_template_root).getTag(); //??
unSelectAllButtons();
changeColor(selectedButton);
Repository.findById(questionId).selectAnswer(selectedButton.getId());
}
Where unSelectAllButtons represents buttonToUnSelect.getBackground().clearColorFilter(); on the four buttons. and Repository is just a static class with example question data.
It all goes terribly wrong, when I have more then one view. On each fragment I inflate the same xml with same View IDs, as I have defined them. And as I now understand calling findViewById retrieves not one, but all views with that Id from my current, but also from my previous and next fragment as well. So every time I want to select my current fragment's view, I also modify the same view in the previous and next fragments as well. You can imagine how this is problematic. This makes me feel I have a fundamental mistake, because I don't think there is supposed to be more then one View with same ID.
I really don't understand how I should do this using ViewPager. At this point it feels like I'm trying to make a wood carving, but instead I am hacking the framework to pieces. There must be a better way to do this with ViewPager.
RESOLVED: Thanks to Soo Chun Jung for pointing me to the answer. In short what got it working for me was:
Passing my Question model id to each fragment with Bundle.
Storing each fragment in inside an ArrayMap with fragment position as key and fragment as value.
Getting each individual fragment from my selectAnswer function is now easy: first get the current fragment's position with myViewPager.getCurrentItem, then calling getter function which returns a fragment on the current position.
Now that I have the fragment I can easily change its button's because they are kept as private fields, assigned in the 'onCreateView` method.
Hope it's helpful~
adapter
class CustomAdapter extends FragmentPagerAdapter {
private final String[] TITLES = {"A", "B"};
private final String TAG = CustomAdapter.class.getSimpleName();
private final ArrayList<Fragment> mFragments;
private final FragmentManager fm;
public CustomAdapter(FragmentManager fm) {
super(fm);
mFragments = new ArrayList<>(getCount());
this.fm = fm;
}
#Override
public CharSequence getPageTitle(int position) {
return TITLES[position];
}
#Override
public int getCount() {
return TITLES.length;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
Log.d(TAG, "destroyItem position = " + position);
mFragments.remove(object);
super.destroyItem(container, position, object);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
mFragments.add((Fragment) object);
return object;
}
#Override
public Fragment getItem(int position) {
Log.d(TAG, "getItem position = " + position);
if (position == 0) {
return MyFragmentA.newInstance();
} else if (position == 1) {
return MyFragmentB.newInstance();
}
return null;
}
public MyFragmentA getMyFragmentA() {
synchronized (mFragments) {
for (Fragment f : mFragments) {
if (f instanceof MyFragmentA) {
return (MyFragmentA) f;
}
}
}
return null;
}
public MyFragmentB getMyFragmentB() {
synchronized (mFragments) {
for (Fragment f : mFragments) {
if (f instanceof MyFragmentB) {
return (MyFragmentB) f;
}
}
}
return null;
}
}
Fragment class
public class MyFragmentB extends Fragment {
...
public updateYourUI(){
//update something
}
}
Usage
mPager = (CustomViewPager) findViewById(R.id.pager);
mAdapter = new CustomAdapter(getChildFragmentManager());
mPager.setAdapter(mAdapter);
mAdapter.getMyFragmentB().updateYourUI();
for your comment below If you only have one kind Fragment. You can modify some function like this.
public static MyFragmentB newInstance(int ID) {
MyFragmentB fragment = new MyFragmentB();
Bundle bundle = new Bundle();
bundle.putInt("ID", ID);
fragment.setArguments(bundle);
return fragment;
}
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
myID = getArguments().getInt("ID");
....
}
public int getMyID() {
return myID;
}
public MyFragmentB getMyFragmentByID(String id) {
synchronized (mFragments) {
for (Fragment f : mFragments) {
if (f instanceof MyFragmentB) {
MyFragmentB temp = (MyFragmentB)f;
if(temp.getID.equals(id){
return (MyFragmentB) f;
}
}
}
}
return null;
}

How to update TabLayout when returning from an activity

My app I am working on has 3 tabs in TabLayout. Each is comprised of a listview. Additionally I am using a FragmentStatePageAdapter and a custom adapter. When you click on an item in the ListView it launches an activity. After you have completed a task in the activity it updates some global array data. Next the user could hit the back button on their phone to return. This is where the issue resides.
Now Tab1 never changes, I later wish to implement that returning sets it to Tab1. That's a later worry. The problem is that the middle tab does not reload. I understand the reason is because the middle tab is already created so visually the swipe effect looks accurate. Tab3 will update if coming from Tab1 because it was not loaded yet.
I have tried various listeners, working with onResume in the tablayout Fragment. I do not have notifydatasetchanged anywhere in my code and haven't been able to implement that. I presume I have somehow implemented something wrong and overpassed the correct solution being I have tried so many things.
Here is some code to help understand how I have everything setup.
MainActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create the adapter that will return a fragment for each of the three primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(container);
mViewPager.setAdapter(mSectionsPagerAdapter);
mViewPager.setOffscreenPageLimit(0);
//more
final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
tabLayout.setScrollPosition(0, 0f, true);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
SectionsPagerAdapter
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
protected Context mContext;
public SectionsPagerAdapter( FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
int number = position;
switch (number) {
case 0: // Fragment # 0 - This will show FirstFragment
return TabFragment1.newInstance();
case 1: // Fragment # 0 - This will show FirstFragment different title
return TabFragment2.newInstance();
case 2: // Fragment # 1 - This will show SecondFragment
return TabFragment3.newInstance();
default:
return null;
}
// return PlaceholderFragment.newInstance(position + 1);
}
#Override
public int getCount() {
// Show 3 total pages.
return 3;
}
#Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "All";
case 1:
return "Unfinished";
case 2:
return "Completed";
}
return null;
}}
MyAdapter
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
protected Context mContext;
public SectionsPagerAdapter( FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
int number = position;
switch (number) {
case 0: // Fragment # 0 - This will show FirstFragment
return TabFragment1.newInstance();
case 1: // Fragment # 0 - This will show FirstFragment different title
return TabFragment2.newInstance();
case 2: // Fragment # 1 - This will show SecondFragment
return TabFragment3.newInstance();
default:
return null;
}
// return PlaceholderFragment.newInstance(position + 1);
}
#Override
public int getCount() {
// Show 3 total pages.
return 3;
}
#Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "All";
case 1:
return "Unfinished";
case 2:
return "Completed";
}
return null;
}}
Fragment1 (all three fragments are the exact same)
public class TabFragment1 extends android.support.v4.app.Fragment {
public ArrayList<String> mylist1 = new ArrayList<>();
// newInstance constructor for creating fragment with arguments
public static TabFragment1 newInstance() {
TabFragment1 fragmentFirst = new TabFragment1();
Bundle args = new Bundle();
//pass a list or something?
fragmentFirst.setArguments(args);
return fragmentFirst;
}
// Store instance variables based on arguments passed
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
// Inflate the view for the fragment based on layout XML
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_main, container, false);
mylist1.clear();
for (int i = 0; i < arrayInt.length; i++) {
mylist1.add(array[i]);
}
final ListView listView = (ListView) rootView.findViewById(R.id.text_label);
MyAdapter adapter = new MyAdapter(getContext(), mylist1);
listView.setAdapter(adapter);
//Click on Item
//selecting items
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//goes to new activity passing the item name
Intent intent = new Intent(view.getContext(), MapsActivity.class);
Bundle b = new Bundle();
//get text for current item
//String textGet = listView.getItemAtPosition(position).toString();
String textGet = mylist1.get(position);
//put text into a bundle and add to intent
intent.putExtra("text", textGet);
//get position to carry integer
intent.putExtra("position", position);
intent.putExtras(b);
//begin other activity
startActivity(intent);
}
});
return rootView;
}
Here is an older attempt at notifying data set changed but no luck. I put this in the fragment itself.
public void onResume(){
super.onResume();
Log.e("Frag2", "Refresh Test");
// a check so it doesnt run first time starting app and crashing
if (g != 0) {
updateData();
}
g = 1;
}
private void updateData(){
Log.e("Frag2", "Refresh Test");
this.mylist2.clear();
for (int i = 0; i < arrayInt.length; i++) {
if (arrayInt[i] == 1) {
this.mylist2.add(array[i]);
}
}
adapter = new MyAdapter(getContext(), this.mylist2);
adapter.notifyDataSetChanged();
}

Fragment State Pager Adapter Showing Wrong Fragment

I have a FragmentStatePagerAdapter which shows the details of current Fragment in the next Fragment. Here is the Page Adapter
public class CustomerPagerAdapter extends FragmentStatePagerAdapter
{
public SectionsPagerAdapter(FragmentManager fm)
{
super(fm);
}
#Override
public Fragment getItem(int position) {
Gson gson = new Gson();
Customer selectedCustomer = mCustomers.get(position);
String serializedCustomer = gson.toJson(selectedCustomer);
return CustomerDetailsFragment.newInstance(serializedCustomer);
}
#Override
public int getCount()
{
return mCustomers.size();
}
#Override
public CharSequence getPageTitle(int position)
{
Customer selectedCustomer = mCustomers.get(position);
String CustomerTitle = selectedCustomer.getFirstName() + " " + selectedCustomer.getLastName();
return CustomerTitle;
}
}
And here is the Fragment where the detail are displayed
public static class CustomerDetailsFragment extends Fragment {
private Customer passedInCustomer;
public CustomerDetailsFragment() {
// Required empty public constructor
}
public static CustomerDetailsFragment newInstance(String serializedCustomer){
CustomerDetailsFragment fragment = new CustomerDetailsFragment();
if (serializedCustomer != null && !serializedCustomer.isEmpty()){
Bundle args = new Bundle();
args.putString("customerInfo", serializedCustomer);
fragment.setArguments(args);
}
return fragment;
}
private void getPosition(){
Bundle args = getArguments();
if (args != null && args.containsKey("customerInfo")){
String serializedCustomer = (getArguments().getString("customerInfo"));
Gson gson = new Gson();
passedInCustomer = gson.fromJson(serializedCustomer, Customer.class);
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
mRootView = inflater.inflate(R.layout.fragment_Customer_details, container, false);
if (passedInCustomer != null) {
showCustomerInfo();
}
return mRootView;
}
}
The problem stems from the fact that getItem is called twice for each swipe and the information that need to be displayed are contained in an List so when getItem is called twice the second object in the list is fetched and displayed in the screen where the first object was supposed to be displayed.
Has anyone dealt with displaying an nth number of items in a scrollable ViewPager where you have to create the Fragments on demand. If yes, can you give me suggestions how to deal with this.
Thanks
I notice that this problem is showing only when I have same fragments with diferent values ( what is probably in the most case ). So after long searching for right answer, i decided to in getItem() method every odd time return one fragment and every even time return another fragment ( These fragment are completely same, except their names:
#Override
public Fragment getItem(int position) {
if(position % 2 == 0){
return FragmentGalleryImage.newInstance(images.get(position), activity);
}else{
return FragmentGalleryImage2.newInstance(images.get(position), activity);
}
}
And i solved problem nice and smooth.
I solved the problem by calling on pager.getCurrentItem() to get the position of the currently displayed item.

When creating two instances of the same fragment and registering them only the second one created gets event

I have created two fragments in a ViewPager , when I click on first one , Second fragment is taking the click.
This issue puts me in another position, when I create two instance from same fragment but with different data.
{
#Override
public Fragment getItem(int index) {
switch (index) {
case 1:
return FragmentBrandList.getInstance(tabs.getBrandList2(), 19,
title);
case 0:
return FragmentBrandList.getInstance(tabs.getBrandList1(), 19,
title);
}
return null;
}
#Override
public int getCount() {
return 2;
}
}
After creating ViewPager , both the fragments get created correctly , but when I click on any thing in the first fragment , the click event gets fired in second fragment not in the first fragment.
EDIT
public class TabsPagerAdapter extends FragmentPagerAdapter {
public TabsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int index) {
switch (index) {
case 1:
return FragmentBrandList.getInstance(tabs.getBrandList2(), 19,
title);
case 0:
return FragmentBrandList.getInstance(tabs.getBrandList1(), 19,
title);
}
return null;
}
#Override
public int getCount() {
return 2;
}
in FragmentBrandList
public class FragmentBrandList extends Fragment {
ArrayList<Brand> brandList = new ArrayList<Brand>();
int discoverID;
RecyclerView listView;
LinearLayoutManager mLayoutManager;
public static FragmentBrandList getInstance(ArrayList<Brand> brandList,
int discoverID, String title) {
FragmentBrandList frag = new FragmentBrandList();
Bundle b = new Bundle();
b.putSerializable("brandList", brandList);
b.putInt("discoverID", discoverID);
b.putString("title", title);
frag.setArguments(b);
return frag;
}
public FragmentBrandList() {
}
String title = "";
View v;
boolean isInflated = false;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (v == null) {
v = inflater.inflate(R.layout.fragment_list_view_brownbg,
container, false);
isInflated = true;
} else {
isInflated = false;
((ViewGroup) v.getParent()).removeView(v);
}
return v;
}
MainActivity activity;
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (isInflated) {
activity = (MainActivity) getActivity();
initView();
}
}
public void initView(){
title = getArguments().getString("title");
discoverID = getArguments().getInt("discoverID");
listView = (RecyclerView) v.findViewById(R.id.listView);
mLayoutManager = new LinearLayoutManager(getActivity());
listView.setItemAnimator(new DefaultItemAnimator());
listView.setHasFixedSize(true);
listView.setLayoutManager(mLayoutManager);
listView.setAdapter(new BrandListRecAdapter(getActivity(),
R.layout.single_item_listview, brandList));
}
#Override
public void onResume() {
// handle on click
((BrandListRecAdapter) listView.getAdapter())
.setOnItemClickListener(new ItemClickListener() {
#Override
public void onItemClickListener(final int pos, View v) {
activity.replaceCurrentFragment(
FragmentBrandDetails.getInstance(
brandList.get(pos), "bank"), true,
true);
}}
EDIT
i think problem cause
when create second fragment , listview.onclick is overwrite first one !!
how can solve this peb?
EDIT
thank you to every one try to help me
problem is already because i use same adapter and same fragment
when second fragment created it is overwrite on item click
so when click in item is called second one !!!
Just put this android:clickable="true" in every fragment layout, and this will not happen again.
This is just an educated guess, but because a ViewPager will always create at least one extra Fragment on either side of the currently visible fragment, you may be creating two virtually identical Fragments in parallel, assigning them both onItemClickListeners in onResume and as such they are both responding to item clicks when an item is pressed on either Fragment.
You could try moving the onItemClickListener to the ViewHolder in your Adapter, rather assigning it in onResume. In addition, I wonder what a Brand object looks like in your RecyclerView, and whether it wouldn't be simpler to pass the current ViewPager page as a parameter in getInstance, and use this to access an Array containing the information necessary to fill your RecyclerView rows.
Here is a very brief example of how your ViewHolder may look:
class MyRecyclerViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener
public MyRecyclerViewHolder(View itemView) {
itemView.setOnClickListener(this);
//etc.

Fragment in ViewPager not restored after popBackStack

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)
}
}

Categories

Resources