Please refer to this question for the setup of fragments:
| A | B |
↓
| C | D |
↓
| E |
I am struggling to figure out why my App is exiting on pressing the back button on a fragment added with addToBackStack().
According to the answer,
Case 1: If I use getSupportFragmentManager() or getFragmentManager(): my fragments vanish after I swipe to a different tab and come back.
Case 2: if I use getChildFragmentManager(): I dynamically add Fragment E to C while using addToBackStack() but when I press back, the app exits. Expectation is that it should return to C instead of exiting.
Code for adding tabs C and D is:
public class MyProfileTabFragmentPagerAdapter extends FragmentPagerAdapter {
private String tabTitles[];
private Context context;
private String userID;
private static final String TAG = makeLogTag(MyProfileTabFragmentPagerAdapter.class);
public MyProfileTabFragmentPagerAdapter(FragmentManager fm, Context context) {
super(fm);
tabTitles = context.getResources().getStringArray(R.array.profileTabs);
this.context = context;
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
userID = prefs.getString("token", "");
}
#Override
public int getCount() {
return tabTitles.length;
}
#Override
public Fragment getItem(int position) {
switch (position){
case 0:
{
return UserProfileFragment.getInstance(userID);
}
case 1:
{
return new MySnapsFragment();
}
default:
{
LOGI(TAG, "Invalid tab" + position);
return null;
}
}
}
#Override
public CharSequence getPageTitle(int position) {
// Generate title based on item position
return tabTitles[position];
}
}
This is how I setup my tabs in B which I call in its onCreateView():
private void setupTabs() {
// Get the ViewPager and set it's PagerAdapter so that it can display items
vpProfileTab.setAdapter(new MyProfileTabFragmentPagerAdapter(getChildFragmentManager(), ctx));
// Give the TabLayout the ViewPager
slidingProfileTabs.setDistributeEvenly(true);
slidingProfileTabs.setBackgroundColor(colorAccent2);
slidingProfileTabs.setSelectedIndicatorColors(colorTabIndicator);
slidingProfileTabs.setViewPager(vpProfileTab);
}
I setup C from B using in onCreateView():
private void setupUserProfileFeed() {
if(feedFragment==null){
feedFragment = new FeedFragment();
this.getFragmentManager().beginTransaction().replace(R.id.feed_fragment, feedFragment).addToBackStack()
.commit();
}
}
I setup E from C when I click a button:
#Override
public void onProfileClick(View v) {
UserProfileFragment userProfileFragment = UserProfileFragment.getInstance(userID);
this.getFragmentManager().beginTransaction().replace(R.id.feedContent, userProfileFragment)
.addToBackStack(null).commit();
}
I am using SlidingTabLayout from Google github here.
I have the following questions:
What is the reason for Case 1. Which FM should be used, as per my understanding: getFM() should be used for top level and getChildFM() should be used for adding fragments to a fragment?
Why is the App exiting in Case 2? Do I need to do anything else?
How to solve this problem either way? Have been stuck for a while, any help is highly appreciated.
override onBackPressed and make a switch statement for the different view pager positions (0,1,2,3...) and tell it what to do in each case. Add this to the main activity where you are attaching the viewPager adapter. This example code is a simple way to do it if you want more complex behavior use the switch statement as previously described.
#Override
public void onBackPressed() {
if (mViewPager.getCurrentItem() == 0) {
// If the user is currently looking at the first page, allow android to handle the
// Back button. This exits the app because you are on the first fragment.
super.onBackPressed();
} else {
// Otherwise, select the fragment in the viewPager
mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1);
}
}
Related
When I start the app everything works ok but when I rotate to landscape it crashes because in the Fragment there is a field that is NULL.
I dont use setRetainInstance(true) or adding Fragments to FragmentManagerI create new Fragments on app start and when app rotate.
In the Activity OnCreate() I create the Fragment and adding them to the viewPager like this.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ParentBasicInfoFragment parentBasicInfoFragment = new ParentBasicInfoFragment();
ParentUTCFragment parentUTCFragment = new ParentUTCFragment();
ParentEventsFragment parentEventsFragment = new ParentEventsFragment();
this.mFragments = new ArrayList<>();
this.mFragments.add(parentBasicInfoFragment);
this.mFragments.add(parentUTCFragment);
this.mFragments.add(parentEventsFragment);
this.viewpage.setOffscreenPageLimit(3);
setCurrentTab(0);
this.viewpage.setAdapter(new MainActivityPagerAdapter(getSupportFragmentManager(), this.mFragments));
}
Then I have a test button on the app that when I press it will do like
public void test(View view) {
((BaseFragment) MainActivity.this.mFragments.get(MainActivity.this.viewpage.
getCurrentItem())).activityNotifiDataChange("hello");
}
This will work and the current Fragments in the ViewPager have the method, activityNotifiDataChange() that are being called and all is ok.
When I rotate the app and do the same thing pressing the button the activityNotifiDataChange() is being called alright but there a null pointer exception because the ArrayList<Fragment> mFragment is now NULL.
Here´s a small sample Android Studio project showing this behavior:
https://drive.google.com/file/d/1Swqu59HZNYFT5hMTqv3eNiT9NmakhNEb/view?usp=sharing
Start app and press button named "PRESS TEST", then rotate device and press the button again and watch the app crash
UPDATE SOLUTION thanks #GregMoens and #EpicPandaForce
public class MainActivityPagerAdapter extends PersistenPagerAdapter<BaseFragment> {
private static int NUM_ITEMS = 3;
public MainActivityPagerAdapter(FragmentManager fm) {
super(fm);
}
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return ParentBasicInfoFragment.newInstance(0, "Page # 1");
case 1:
return ParentUTCFragment.newInstance(1, "Page # 2");
case 2:
return ParentEventsFragment.newInstance(2, "Page # 3");
default:
return null;
}
}
}
public abstract class PersistenPagerAdapter<T extends BaseFragment> extends FragmentPagerAdapter {
private SparseArray<T> registeredFragments = new SparseArray<T>();
public PersistenPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
public T instantiateItem(ViewGroup container, int position) {
T fragment = (T)super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public T getRegisteredFragment(ViewGroup container, int position) {
T existingInstance = registeredFragments.get(position);
if (existingInstance != null) {
return existingInstance;
} else {
return instantiateItem(container, position);
}
}
}
The main problem I see with your app is your misunderstanding with how FragmentPagerAdapter works. I see this a lot and it's due to lack of good javadocs on the class. The adapter should be implemented so that getItem(position) returns a new fragment instance when called. And then getItem(position) will only be called by the pager when it needs a new instance for that position. You should not pre-create the fragments and pass then into the adapter. You should also not be holding strong references to the fragments from either your activity or from parent fragments (like ParentBasicInfoFragment). Because remember, the fragment manager is managing fragments and you are also managing fragments by newing them and keeping references to them. This is causing a conflict and after rotation, you are trying to invoke activityNotifiDataChange() on a fragment that is not actually initialized (onCreate() was not called). Using the debugger and tracking object IDs will confirm this.
If you change your code so that the FragmentPagerAdapter creates the fragments when they are needed and don't store references to fragments or lists of fragments, you will see much better results.
this.mFragments = new ArrayList<>();
Because this is wrong. You should never hold a reference to the fragment list if you are using ViewPager. Just return new instance of Fragment from getItem(int position) of FragmentPagerAdapter and you are good to go.
But to fix your code, you must delete mFragments entirely.
See https://stackoverflow.com/a/58605339/2413303 for more details.
Use setRetainInstance(true) is not a good approach. If you need to same some simple information such as: position of recyclerView, selected item of recyclerView, maybe some model(Parcelable) you could do it with method onSaveInstanceState / onRestoreInstanceState, there is one limitation is 1MB. Here is an example with animations how it works.
For more durable persistance use SharedPreferences, Room(Google ORM) or you could try to use ViewModel with LiveData (best approach some data which should live while user session).
//change in AndroidManifest.xml file,
<activity
android:name=".activity.YourActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="sensor"
/>
//YourActivity in which you defines fragment.
//may be it helps
Hello anyone i have working on a reading application and I have build up a view pager contain three fragment, bookFragment, chapterFragment and verseFragment
I want to update the chapterFragment and verseFragment when user selected one book item, but the fragment UI still not exit, so I can't do refreshUI in OnPagerSelected.
It any way to do this?
follow is my adapter:
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
private int[] title = {R.string.label_book, R.string.label_chapter, R.string.label_verse};
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return bookFragment = new SelectionBookFragment();
case 1:
return chapterFragment = new SelectionChapterFragment();
case 2:
return verseFragment = new SelectionVerseFragment();
}
return new Fragment();
}
#Override
public int getCount() {
return NUM_PAGES;
}
#Override
public CharSequence getPageTitle(int position) {
return getResources().getString(title[position]);
}
}
and the fragment UI is null when I do this:
Fragment fragment = screenSlidePagerAdapter.getItem(CHOOSER_CHAPTER);
fragment.refreshUI() <-- null
Big Thanks!
If the fragment doesn't exist, the method onCreateView() will be called when showing up. You can set book's data in here.
If the fragment exist, you can call refreshUI().
I think you only need to check fragment != null when call refreshUI().
A simple and dirty way would be to force the ViewPager to create all the Fragment instances and keep them, by calling
viewPager.setOffscreenPageLimit(2);
Another way is to save the selected value in SharedPreferences and then retrieve the value when the Page is shown.
SharedPreferences settings = getSharedPreferences(SELECTED_BOOK, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putInt("selectedBook", 3);
I am using viewpager in order to load images from the server, each fragment should load the image based on positon of the fragment in the adapter. so for example position 0 will load image 0 position 1 load image 1 etc. for the last two days I am struggeling with getting the correct fragment position, in total I have 3 fragments however from print outs I have added to the code I can see only position 0 and 2 and thus the image is duplicated in position 1.
the main question is how can I resolve this? I would like to get the correct position and based on that the correct image. below is the viewpager code and adapter code, it was modified several times based on several solutions however none of them seems to work
public class LoadCarFullSizeImage extends AppCompatActivity{
private ViewPager viewPager;
protected String imagePath;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.car_images);
imagePath=getIntent().getStringExtra("imagePath");
viewPager=(ViewPager)findViewById(R.id.vp_carViewPager);
CirclePageIndicator titleIndicator = (CirclePageIndicator)findViewById(R.id.titles);
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager(),imagePath);
viewPager.setAdapter(mPagerAdapter);
titleIndicator.setViewPager(viewPager);
}
#Override
public void onBackPressed() {
if (viewPager.getCurrentItem() == 0) {
// If the user is currently looking at the first step, allow the system to handle the
// Back button. This calls finish() on this activity and pops the back stack.
super.onBackPressed();
} else {
// Otherwise, select the previous step.
viewPager.setCurrentItem(viewPager.getCurrentItem() - 1);
}
}
private static class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter{
private String imgPath;
public ScreenSlidePagerAdapter(android.support.v4.app.FragmentManager fm, String _imagePath) {
super(fm);
this.imgPath=_imagePath;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0: // Fragment # 0 - This will show FirstFragment
return CarImageFragment.newInstance(0, imgPath);
case 1: // Fragment # 0 - This will show FirstFragment different title
return CarImageFragment.newInstance(1, imgPath);
case 2: // Fragment # 1 - This will show SecondFragment
return CarImageFragment.newInstance(2, imgPath);
default:
return null;
}
}
#Override
public int getCount() {
return 3;
}
}
}
Since you are using the indicator.. you have to getthe current page from it... and set current page using it too...
When using indicator stop using the pager or pager listener... any listener should be set to the indicator.
titleIndicator.getCurrentItem()==0...
and
titleIndicator.setCurrent.....
I am aware that when you would like to communicate between fragments you should do so via the parent activity. It makes sense when the two fragments are on the same level. If one is nested within the second It makes little sense to "go up" only to return "down".
In this scenario the pattern makes sense:
Activity
____|____
/ \
Frag A Frag B
It makes little sense to use the pattern when one is nested within the other:
Activity
|
Frag A
|
Frag B
Is it acceptable to communicate directly using findFragmentByTag(FRAG_X_TAG) if one is nested within the other?
The idea of this pattern is to use the Activity as a Controller interface, to which the Fragments are Views - they send UI events to the Controller, which in turn updates them as appropriate.
This means that the real question here should be "Is Frag A a Controller for Frag B" - if so, direct communication would be acceptable. If both are just "dumb Views", they really shouldn't know about each other.
The bottom line is that you want to avoid making spaghetti code, which is possible as long as you enforce separation of concerns.
According to API 4.2 documentation, you should use getChildFragmentManager()
You can always use BusEvent library, it eases communication between components, but you may also lose code intelligibility.
It sounds like you could use an Event Bus. These are some of the most popular choices:
Otto by Square: http://square.github.io/otto/
EventBus by GreenRobot: https://github.com/greenrobot/EventBus
Another option is to use Model–view–viewmodel (MVVM) artchitecture that google promotes in Google Architecture components:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
model.getSelected().observe(getViewLifecycleOwner(), { item ->
// Update the UI.
});
}
}
More info:
https://developer.android.com/topic/libraries/architecture/viewmodel#sharing
If you want to activte morethan 1 fragment at the same time... then use the method getChildFragmentManager()
eg:
I have a main fragment called "ShareSpace" and has 3 child fragment..
//In ShareSpace Fragment
View view = inflater.inflate(R.layout.fragment_sharespace, container, false);
mTabletSize = getResources().getBoolean(R.bool.isTablet);
mContext=getActivity();
ViewPager pager = (ViewPager) view.findViewById(R.id.pager);
ShareSpaceAdapter pagerAdapter = new ShareSpaceAdapter(mContext,getChildFragmentManager());
pager.setAdapter(pagerAdapter);
mSlidingTabLayout = (SlidingTabLayout) view.findViewById(R.id.sliding_tabs_share);
mSlidingTabLayout.setViewPager(pager);
//In Adapter
public ShareSpaceAdapter(Context mContext, FragmentManager fm) {
super(fm);
this.mContext = mContext;
// TODO Auto-generated constructor stub
try {
if (home == null) {
homeFact = FactoryGenerator.getFactory(Constants.HOME);
home = homeFact.getHomeManagement(Constants.SHARESPACE);
}
local = home.readAssets(mContext);
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public Fragment getItem(int arg0) {
Fragment frgmt = null;
switch (arg0) {
case 0:
frgmt = new ShareSpaceFiles(mContext);
break;
case 1:
frgmt=new ShareSpaceFolder(mContext);
break;
case 2:
frgmt = new ShareSpaceInbox(mContext);
break;
default:
break;
}
return frgmt;
}
#Override
public int getCount() {
return PAGE_COUNT;
}
#Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0: // Fragment # 0 - This will show FirstFragment
return local.getmFiles();//"FILES";
case 1: // Fragment # 0 - This will show FirstFragment different title
return local.getmFolders();//"FOLDERS";
case 2:
return local.getmMessages();//"MESSAGES";
default:
return null;
}
}
It will load 3 child fragment at the same time
Good Morning All,
I am having trouble with my Activity's ActionBar keeping proper functionality after it is left for a period of time. Basically I have different ActionBar views set up dependant on which page in a ViewPager that I'm on. The middle page of 3 implements ActionBar.NAVIGATION_MODE_LIST with a list that is used to filter the content of the ListView on that page. On the other two pages this list is not shown. My code for handling this:
public class SectionsPagerAdapter extends MyPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
Fragment fragment;
if(i==0){
ActionBar bar = MyApp.this.getActionBar();
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
fragment = page1;
}else if(i==1){
fragment = page2;
}else{
fragment = page3;
}
return fragment;
}
#Override
public int getCount() {
return 3;
}
#Override
public void onPageSelected(int position) {
if(position== 1 && menuSearch != null){
menuSearch.setVisible(true);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
}else{
if(menuSearch != null){
menuSearch.setVisible(false);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
}
}
currentPage = position;
}
public int getCurrentPage() {
return currentPage;
}
I am currently able to force the failure for testing by starting a new activity, which I can then force close, and when I come back to this main activity the ActionBars drop down menu is shown on every page. The menu also loses connection to the filtering properties it performed in 2nd page.
UPDATE 1:
I now believe that my problem lies with a disconnect between my ViewPager, ActionBar and the Fragments they control. I added the following code to the onNavigationItemSelected portion of my Activity:
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
Log.i(TAG,"Fragment ID:" + String.valueOf(fragment.getId()));
if(fragment.isAdded()){
Log.i(TAG,"fragment.isAdded");
}
When I first run the app these Logs return the following:
Fragment ID: 2131492869
fragment.isAdded
However, after I force a crash or leave the app and come back later, the Logs return
Fragment ID: 0
The fragment.isAdded is false at this point, but I'm not sure why.
Any help would be greatly appreciated.
Thanks,
Josh
Can anyone give me insight about why this may be happening? Should I be saving the actionbar state somehow in onPause and then restoring in onResume?
Yes, this is happening becuase you not save the last index ,
so basically what you need to do is
in onResume
set the viewPager current index like this : viewPager.setCurrentItem(lastFragmentIndex);
I think you need to save your fragments in onSaveInstanceState and restore them in onRestoreInstanceState like this:
protected void onSaveInstanceState(Bundle outState) {
FragmentManager fm = getSupportFragmentManager();
fm.putFragment(outState, 1, fragment1);
fm.putFragment(outState, 2, fragment2);
}
and restore
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
fragment1 = (Fragment) fm.getFragment(savedInstanceState, 1);
}