Trying to dynamically add Fragments to different pages in an Android ViewPager.
Dynamically adding a Fragment to the end of the list of Fragments works fine, but attempting to add to the next slot (the unseen page after the current viewable page). I've tried adding to the arraylist of fragments and using notifyDataSetChanged and trying to use set on each of the fragments after to set each to the previous item.
Edit: updated code
class MainPagerAdapter extends FragmentPagerAdapter {
public MainPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public PageFragment getItem(int position) {
return PageFragment.newInstance(Datamart.getInstance().getURLs().get(position));
}
#Override
public int getCount() {
return Datamart.getInstance().getURLs().size();
}
#Override
public int getItemPosition(Object object) {
PageFragment pageFragment = (PageFragment) object;
for ( int i = 0; i < getCount(); i++ ) {
if ( pageFragment.getURL().equals( Datamart.getInstance().getURLs().get(i) ) ) {
return i;
}
}
return POSITION_NONE;
}
}
Within the Datamart:
public class Datamart {
static Datamart instance;
private ArrayList<String> URLs;
private MainPagerAdapter mainPagerAdapter;
private ViewPager viewPager;
public Datamart() {
URLs = new ArrayList<>();
}
public static Datamart getInstance() {
if (instance == null) {
instance = new Datamart();
}
return instance;
}
public MainPagerAdapter getMainPagerAdapter() {
return mainPagerAdapter;
}
public void setMainPagerAdapter(MainPagerAdapter mainPagerAdapter) {
this.mainPagerAdapter = mainPagerAdapter;
}
public ViewPager getViewPager() {
return viewPager;
}
public void setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
}
public void addToEnd( String URL ) {
URLs.add(URL);
mainPagerAdapter.notifyDataSetChanged();
}
public void addNext( String URL ) {
URLs.add(viewPager.getCurrentItem() + 1, URL);
mainPagerAdapter.notifyDataSetChanged();
}
public ArrayList<String> getURLs() {
return URLs;
}
public void setURLs(ArrayList<String> URLs) {
this.URLs = URLs;
}
}
ViewPager is doing its darndest to try and keep track of where all the fragments go, and you got it confused because you inserted a fragment in the middle without telling it.
Let's do an example. Say that you have two pages so that you are displaying the first page and the second page is the offscreen page to the right. You have fragment0 for the first page and fragment1 for the second page.
First, keep in mind that since ViewPager is managing the offscreen pages to the immediate left and right, it already thinks it knows what the second page fragment is, which is fragment1.
When you insert a fragment between those existing page fragments, ViewPager is not going to ask you right away which fragment is at the second position, it's going to ask you if the fragment at the second position has changed.
This is where getItemPosition() comes in. You didn't implement getItemPosition() in your FragmentPagerAdapter, so the default behavior of getItemPosition() is to return POSITION_UNCHANGED. (As you'll see, this is why adding a fragment at the end doesn't cause an error.)
So when you called notifyDataSetChanged(), the ViewPager called getCount() and saw that instead of two pages, you now have three. It then called getItemPosition() for page 0 and page 1 (the pages it knew about) and your default implementation told it that nothing had changed.
But something did change, you inserted a page/fragment in the middle of the two existing pages. ViewPager asked you if the second page changed, and your getItemPosition() should have returned 2 instead of POSITION_UNCHANGED.
Now because your getCount() returned 3 instead of 2, ViewPager knows that there is a new page at the end that it doesn't know about, so it calls getItem() to get the new fragment.
Here is where the error occurs. Your getItem() method returned fragment1 for page 3, and ViewPager choked because you handed it the fragment that it already knew about. Specifically, it uses the fragment's tag for some housekeeping. It creates a tag out the the page's container id and the item position (page number). So at any given time it expects that either (a) the tag is null, or (b) the tag will be the same value that it already calculated from the container id and the item position. Neither of those conditions were true, so that's why IllegalStateException was thrown.
So how do you fix this?
You implement a getItemPosition() that does the right thing. When your getItemPosition() is called, you scan through your list of fragments, and when you find a matching fragment, you return the index of the fragment. If you removed a fragment such that there's no matching fragment in your list, you return POSITION_NONE to tell the ViewPager that the page should be removed. ViewPager will then call your getItem() method to get the new page. In this way, ViewPager can always stay in sync with your adapter.
When I write a FragmentPagerAdapter, I don't even use the fragments in the model. If I were writing your adapter, I would just have a list of URLs, since it looks like each page/fragment has it's own unique URL.
I would also implement a getUrl() method on the fragment class to get the URL that the fragment was created with.
So for getItemPosition(), I would call getUrl() on the fragment passed in, and search my adapter's list for that URL. Then I would return the index of the URL in the list, or POSITION_NONE if it's not there any more.
Then in getItem(), I would instantiate a new fragment using the URL at the list position that was passed in. My rule is that I never instantiate a fragment until the ViewPager specifically requests it by calling getItem().
This way, only the ViewPager is accessing the fragments, and I don't run into these types of issues.
Also, don't make any changes to the adapter model between the time when ViewPager calls startUpdate() to when it calls finishUpdate(). This is how ViewPager tells your adapter that it's in a "critical section" of figuring out where all the page fragments go.
PagerAdapters can be a bear to work with. Put some debug logging in your getCount(), getItemPosition(), and getItem() methods so that you can get a feel for how ViewPager uses your adapter to manage its pages.
Related
I have a Parent Fragment, called WaterFountainFragment, that has a nested Fragment progamatically inflated inside a FrameLayout that is dependant of an user's choice in a RadioGroup. If the user chooses one option, it inflates one Child and, when chosen the other option, it inflates another child (in the name of being the most concise as possible, I'll only list one of those child fragments, since the problem happens in both of them).
The user enters the data he wants and save it on the database, using LiveData and Room dependencies to do so. However, if the user wants to go back and check which data was saved in an specific entry, then we start to face trouble.
The problem is, the saved data is shown in the Parent Fragment but, unfortunately, it does NOT load inside the child fragment nested on it.
First, let me show the parent class in which I think everything is working normally:
WaterFountainFragment Class
public class WaterFountainFragment extends Fragment {
(...)
#Override
public void onStart() {
super.onStart();
//modelEntry is a ViewModel, which class ViewModelEntry will be shown later
modelEntry = new ViewModelEntry(requireActivity().getApplication());
//The ID of an entry is sent to this fragment through a bundle (via setArgument());
if (waterFountainBundle.getInt(FOUNTAIN_ID) > 0) {
//getOneWaterFountain is a method inside modelEntry to obtain an entry from the database through
//LiveData and Room
modelEntry.getOneWaterFountain(waterFountainBundle.getInt(FOUNTAIN_ID))
.observe(getViewLifecycleOwner(), this::loadFountainInfo);
}
}
//this is a RadioGroup Listener where it inflates the specific child fragment inside a FrameLayout
public void typeFountainListener(RadioGroup group, int checkedID) {
int index = getCheckedFountainType(group);
switch (index) {
case 0:
getChildFragmentManager().beginTransaction()
.replace(R.id.water_fountain_info, new WaterFountainSpoutFragment()).commit();
break;
case 1:
getChildFragmentManager().beginTransaction()
.replace(R.id.water_fountain_info, new WaterFountainOtherFragment()).commit();
break;
default:
break;
}
}
//method to fill all fields inside this fragment
private void loadFountainInfo(WaterFountainEntry waterFountain) {
fountainLocationValue.setText(waterFountain.getFountainLocation());
typeWaterFountain
.check(typeWaterFountain.getChildAt(waterFountain.getTypeWaterFountain()).getId());
if (waterFountain.getFountainTypeObs() != null)
fountainTypeObsValue.setText(waterFountain.getFountainTypeObs());
//At this point the RadioGroupListener is active already and, when checking a RadioButton, it will
//inflate the child and send a fragmentResult to this child fragment
getChildFragmentManager()
.setFragmentResult(InspectionActivity.LOAD_CHILD_DATA, waterFountainBundle);
}
}
Now we have one of the 2 nested child fragments that are inflated:
WaterFountainSpoutFragment Class
public class WaterFountainSpoutFragment extends Fragment {
#Override
public void onStart() {
super.onStart();
//the ViewModel is instantiated here with the same name and method as the
//one instantiated in the ParentFragment
modelEntry = new ViewModelEntry(requireActivity().getApplication());
//FragmentResultListener is active and it does recieve the signal from the parent Fragment.
getParentFragmentManager()
.setFragmentResultListener(InspectionActivity.LOAD_CHILD_DATA, this, (key, bundle) ->
//However, inside this resultListener, this observer seems to not be triggered
modelEntry.getOneWaterFountain(bundle.getInt(WaterFountainFragment.FOUNTAIN_ID))
.observe(getViewLifecycleOwner(), this::loadSpoutFountainData)
);
}
}
Here we have the ViewModel, the repository and the Dao classes/interfaces that were used on those 2 classes above
ViewModelEntry class
public class ViewModelEntry extends AndroidViewModel {
public ViewModelEntry(#NonNull Application application) {
super(application);
repository = new ReportRepository(application);
allEntries = repository.getAllSchoolEntries();
}
public LiveData<WaterFountainEntry> getOneWaterFountain(int waterFountainID) {
return repository.getOneWaterFountain(waterFountainID);
}
}
ReportRepository Class
public class ReportRepository {
private ReportDatabase db;
private final WaterFountainDao waterFountainDao;
public ReportRepository(Application application) {
waterFountainDao = db.waterFountainDao();
}
public LiveData<WaterFountainEntry> getOneWaterFountain(int waterFountainID) {
return waterFountainDao.getOneWaterFountain(waterFountainID);
}
}
WaterFountainDao Interface
#Dao
public interface WaterFountainDao {
#Query("SELECT * FROM WaterFountainEntry WHERE waterFountainID == :waterFountain")
LiveData<WaterFountainEntry> getOneWaterFountain(int waterFountain);
}
What I know/tested so far
Using a Toast, I confirmed that getParentFragmentManager().setFragmentResultListener() is being called. Even more so, the bundle recieve the right data.
If I use the modelEntry.getOneWaterFountain(bundle.getInt(WaterFountainFragment.FOUNTAIN_ID)).observe(getViewLifecycleOwner(), this::loadSpoutFountainData) outside the resultListener, it does load the correct data into the child Fragment fields.
The data entered by the user IS being stored in the database. I confirmed that using the Database Inspector, so it is not a case where "the data is not being stored properly, hence why is not loading".
I use the same method in other Parent/Child Fragments, using the same format of resultListener and it DOES load the data.
Using another method for creating this ViewModel, like modelEntry = new ViewModelProvider.AndroidViewModelFactory(requireActivity().getApplication()).create(ViewModelEntry.class); in both parent and child fragments results in the same problem
What I SUPPOSE it might be the case
I have wondered that it might be a situation where I am choosing the wrong LyfecycleOwner but I don't know if that is the case, mainly because of what I put on item 4.
Any help would be greatly appreciated.
I have a FrameLayout which can be filled with different fragments based upon the user's choice on a RadioGroup. However, I am quite certain that the method I am using to remove such fragments is far from ideal and if possible, I would like some insights on how to make this properly.
How I am removing the fragments right now:
(...)
//Inside the body of the OnViewCreated
FragmentManager manager = getChildFragmentManager();
//RadioGroup listener to show a fragment based on the user's choice
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
//getCheckedRadio is only a method I created to get the index of the option chosen by the user
int index = getCheckedRadio(group);
switch (index) {
case 1:
getChildFragmentManager().beginTransaction().replace(R.id.child_fragment, new FragmentA()).commit();
break;
case 2:
getChildFragmentManager().beginTransaction().replace(R.id.child_fragment, new FragmentB()).commit();
break;
case 3:
getChildFragmentManager().beginTransaction().replace(R.id.child_fragment, new FragmentC()).commit();
break;
default:
removeFragments();
break;
}
(...)
//Method I use to remove the fragments
public void removeFragments() {
try {
FragmentA fragA = (FragmentA) manager.findFragmentById(R.id.child_fragment);
if (fragA != null)
manager.beginTransaction().remove(fragA).commit();
} catch (Exception e) {
try {
FragmentB fragB = (FragmentB) manager.findFragmentById(R.id.child_fragment);
if (fragB != null)
manager.beginTransaction().remove(fragB).commit();
} catch (Exception f) {
try {
FragmentC fragC = (FragmentC) manager.findFragmentById(R.id.child_fragment);
if (fragC != null)
manager.beginTransaction().remove(fragC).commit();
} catch (Exception g) {
Toast.makeText(getContext(), "Try Again", Toast.LENGTH_SHORT).show();
}
}
}
}
However, I know that this method is far from perfect because, should I call it at any point outside of radioGroup.SetOnCheckedChangeListener, it won't remove the fragment being exposed at the time. So my question is: Which is the correct way to remove a fragment from a FrameLayout that can hold different types of Fragments? And why my removeFragments() method works when used on this listener but not when called at other points in the code?
Which is the correct way to remove a fragment from a FrameLayout that can hold different types of Fragments?
First
No need to be bothered by what types of fragments that can be hosted by a FrameLayout placeholder; at the end of the day they are all Fragments (i.e. subclasses of the Fragment class).
Second
Not sure if there is a misconception of the fragment transaction concept; as the name of the method removeFragments() implies to remove multiple fragment(s) from a single placeholder at a time; and the logic of the method applies that to FragmentA, B, & C (i.e. there is a thought that more than one fragment can exist at a time); and this is not right; as at a time, a single placeholder/container can hold one and only one fragment.
This is something different than the back stack which can have multiple instances of the fragments according to the user navigation.
So, the removeFragments() actually need to be removeFragment() where only one fragment need to be removed at a time.
I guess that you thought that replacing a fragment keeps the old one in the placeholder, as per documentation:
Calling replace() is equivalent to calling remove() with a fragment in a container and adding a new fragment to that same container.
Third
There is no need to wrap the remove(fragment) into a try catch block; this method doesn't through any exception at all, so you will never get into the catch block; even if the fragment isn't the current fragment of the container, then it silently discard it.
So, the logic in removeFragments() can be simplified to:
public void removeFragment() {
Fragment fragment = manager.findFragmentById(R.id.child_fragment);
if (fragment != null)
manager.beginTransaction().remove(fragment).commit();
}
should I call it at any point outside of radioGroup.SetOnCheckedChangeListener
This depends on the logic/event you want to use to remove the fragment.
I am trying to use filter in Fragment and implementing the dialog fragment.
This is the class that I am using
public class HomeFragment extends Fragment implements
FilterDialogFragment.FilterListener,
PostAdapter2.OnPostSelectedListener{ detail code }
this the dialogfragment based class for spinner choosing options
public class FilterDialogFragment extends DialogFragment
this method is called upon clicking the filter button, which pops up dialog for spinner options of the filter
Declared
private FilterDialogFragment mFilterDialog;
in onCreateView
mFilterDialog = new FilterDialogFragment();
Method to call
public void onFilterClicked(){
mFilterDialog.show(getSupportFragmentManager(), FilterDialogFragment.TAG);
}
after this upon selecting the spinner option and clicking apply this method is called in which mFilterListener is null which should not be the case
public interface FilterListener {
void onFilter(Filters filters);
}
FilterListener mFilterListener;
public void onSearchClicked() {
Log.d("Message", String.valueOf(mFilterListener));
if (mFilterListener != null) {
Log.d("Message", "New 55555");
mFilterListener.onFilter(getFilters());
}
dismiss();
}
please assist me to solve this problem. if anymore details are required please let me know
attach method in FilterDialogFragement
public void onAttach(Context context) {
super.onAttach(context);
Log.d("Message", "New 6666666");
Log.d("Message", String.valueOf(mFilterListener));
if (context instanceof FilterListener) {
// Log.d("Message", String.valueOf(mFilterListener));
mFilterListener = (FilterListener) context;
}
}
You are attempting to mimic this library implementation: Friendly Eats.
However, you do not copy it wholesale, mainly in that you choose to use HomeFragment which implements FilterDialogFragment.FilterListener to launch FilterDialogFragment, rather than the library's MainActivity. This is the cause of your null pointer.
This is due to how getSupportFragmentManager() works. If you look at Android's documentation for this, you will see it says
Return the FragmentManager for interacting with fragments associated with this activity. (My Bolding)
When you call mFilterDialog.show(getSupportFragmentManager(), FilterDialogFragment.TAG); inside HomeFragment, you are actually calling whatever Activity that is the parent of HomeFragment to launch the new FilterDialogFragment. You could double check this by checking if, in onAttach(Context context) inside HomeFragment, if context instanceof HomeFragment. I do not think it will return true.
To solve this, and without changing your use of HomeFragment, you could simply pass an instance of HomeFragment itself, or a separate implementation of FilterDialogFragment.FilterListener (which I'd prefer if you do not need to use anything from HomeFragment other than the listener) to your FilterDialogFragment instance on creation or before you launch it.
For example, you could create a public setter like so:
private FilterListener mFilterListener;
public void setFilterListener(FilterListener filterListener){
mFilterListener = filterListener;
}
and then in your HomeFragment onCreateView(), you do this:
mFilterDialog = new FilterDialogFragment();
//Or preferably, an anonymous/named implementing instance of the interface only.
mFilterDialog.setFilterListener(this);
Doing so would not rely on the Android framework to provide the initialisation of your field, and does not require you to either change your Activity or HomeFragment you are currently using.
I assume, that u didnt set the listener in a mFilterDialog, so thats why its null
It's quite hard to put My doubt in words but ill try, Hi there I am creating simple book app where user can learn different languages like java,c,c++, and I got and question mark here. my question is how can I control/Inflate more than one RecycleView activity from an adapter class or should I make different RecycleViewand and adapter system for every single language.
i.e I just want an app having multiple buttons with different languages where user can click on the button and learn programming concept listed there he can come back and click another button in order to learn another language as well
"I hope you got my point"
You must use different data
Also you can use different layout for any item
in your model.class (model of data for recyclerView you can define a variable for Type of data
ex :
public String getType() {
return Type;
}
public void setType(String Type) {
this.Type= Type;
}
i use from this setter and getter
so in your addapter class insert below method
#Override
public int getItemViewType(int position) {
return ;
}
with this method you can pass any value you want and get it as int i in myViewHohder onCreateViewHolder method
public myViewHohder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
return ;
}
and you can with a (if) Condition to decide for layout of item
// use case 10b alternate version
// caches a read comment temporarily
public void testCacheReadComment2() throws Throwable{
runTestOnUiThread(new Runnable()
{
#Override
public void run(){
CommentBuilder commentBuilder = new commentBuilder();
Comment comment = commentBuilder.createTopTestComment();
//browse button on main screen
((Button)activity.findViewById(ca.ualberta.cs.team5geotopics.browseButton)).performClick();
//the ListView for the custom adapter
ListView listView = (ListView) activity.findViewById(ca.ualberta.cs.team5geotopics.commentList);
//the custom adapter on the physical screen
ArrayAdapter<Comment> adapter = (ArrayAdapter<Comment>) listView.getAdapter();
adapter.add(comment);
adapter.notifyDataSetChanged();
View view = adapter.getView(adapter.getPosition(comment), null, null);
ViewAsserts.assertOnScreen(listView, view);
//this is the button to view the Top Level comment in the list
ViewAsserts.assertOnScreen(view, (Button) view.viewTopLevelComment);
((Button)view.viewTopLevelComment).performClick();
// is there a way I can get references to the objects
// already instantiated in the test thread?
CacheController cc = activity.getCacheController();
assertTrue(cc.getHistory().contains(comment));
}
});
}
We are using a test driven development style in order to code our project for school. In this test I am trying to prove that after a user views a comment from the list in the adapter, that this comment is cached in a history cache. I'm a little confused about some things and I would like some help, because it would be great if I knew there were no obvious flaws in my test case. these are my questions:
View view = adapter.getView(adapter.getPosition(comment), null, null);
Will this line return the view that is associated with the comment stored in the array adapter? My ArrayAdapter is going to follow a holder patter and I'm not sure if this is the proper way to get access to the buttons I need to mimic the user viewing the comment.
CacheController cc = activity.getCacheController();
I have a CacheController object that is instantiated upon the onCreate() method in our main activity. Now I want to reference this CacheController to see if the history is updated properly. I was just making a new CacheController and mimicking the actions of the CacheController in the test method, but I want to test what happens to my data on the UIthread. So, how do I reference objects in the UI thread?
View view = adapter.getView(adapter.getPosition(comment), null, null);
Will this line return the view that is associated with the comment
stored in the array adapter?
I think it should work, but I don't understand why would you want to access the View.
My ArrayAdapter is going to follow a holder patter and I'm not sure if
this is the proper way to get access to the buttons I need to mimic
the user viewing the comment.
The ArrayAdapter is usually used for a ListView. You should just let ListView handle the click capturing and tell you which element was clicked.
So, how do I reference objects in the UI thread?
You have 2 solutions for this that come to my mind right now:
1) Pass the CacheController instance, for example:
public class YourClass {
private final CacheController cacheController;
public YourClass(final CacheController cacheController) {
this.cacheController = cacheController;
}
public void testCacheReadComment2() throws Throwable {
CacheController cc = this.cacheController;
}
}
2) Singleton: make the CacheController static and put an accessor, for example:
public class CacheController {
private final CacheController instance = new CacheController();
public static CacheController getCacheController() {
return instance;
}
}
In both cases you should be aware about potential multi-threading issues because you're spawning new threads that all share same CacheController instance.