I have created a notes app in which files are stored in internal storage. I have many fragments associated with a single activity. The first fragment has a ListView that shows all the saved files. On clicking on any item of ListView, a new fragment opens that shows the file's data. The app also has a Toolbar with navigation drawer. On pressing the back button of Toolbar from the second fragment, the ListView's scroll position remains the same. However, on clicking the back button (of the OS), the ListView scrolls back to the first item. I am also using the Navigation library to go from one fragment to another.
Here is my Fragment's code containing ListView:
CustomArrayAdapter arrayAdapter;
ArrayList<String> FilesInFolder;
FilesInFolder = GetFiles(getActivity().getFilesDir());
arrayAdapter = new CustomArrayAdapter(FilesInFolder, getContext());
Collections.sort(FilesInFolder);
listView.setAdapter(arrayAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String title = String.valueOf(parent.getItemAtPosition(position));
Bundle bundle = new Bundle();
bundle.putString("fileName", title);
Navigation.findNavController(getView()).navigate(R.id.action_nav_home_to_nav_view_note, bundle);
}
});
Here is my second fragment's back press code:
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
OnBackPressedCallback callback = new OnBackPressedCallback(
true // default to enabled
) {
#Override
public void handleOnBackPressed() {
Navigation.findNavController(getView()).navigate(R.id.action_nav_view_note_to_nav_home);
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(
this, // LifecycleOwner
callback);
}
The CustomArrayAdapter only sets text to each item in ListView and an image next to it.
Now, how can I save or maintain the ListView's scroll position even after going to another fragment and returning again on back press?
Scenario - I am working on an app that shows events respective to particular dates in a viewpager, where each page represents a day(i.e 24 hrs in a vertical manner, similar to google calendar). Each page/fragment contains a vertical scrollview(it has a framelayout inside it) and based on list of events i am dynamically creating custom-views(view position and dimension is based on the corresponding event timing and duration) and adding it to the scrollview. User can drag and drop events between dates.
Issue - I have successfully achieved the view creation part, now the issue is with performance. Sometimes vewpager(using FragmentStatePagerAdapter) lags while swiping through pages.
Someone please suggest me how to reduce the lag or any better ways to achieve this
Yes I had a similar performance with viewpager and FragmentStatePagerAdapter, the problem with viewpager is that pre creates the views either side of the current view to speed up the swipe to next view.
This works well for static views but for views with dynamic data the pre-created view was usually out of date and needed to be regenerated when the user swiped to it.
Thus it was having to call onCreateView on 3 views while the user swiped between views leading to lag sometimes.
I thought of 2 improvements for performance, though only used one.
1) The view holder/model pattern e.g. https://www.androidcode.ninja/android-viewholder-pattern-example/ and https://developer.android.com/topic/libraries/architecture/viewmodel where the inflation and finding items re-used / separated from onCreateView activities.
I did not use this method
2) Create bare bones views for the dynamic ones (The first view was static and then there were 2 dynamic ones at positions 1 and 2 that contained listviews of different aspects of the dynamic data.)
The listviews were inflated and had an adapter set to an empty list in onCreateView of these views thus they were fast to create.
Then I added an on OnPageChangeListener which notified the adapter backing the ViewPager that the pages had changed
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
if (position == 1 || position == 2)
{
mSectionsPagerAdapter.notifyDataSetChanged();
}
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// Code goes here
}
#Override
public void onPageScrollStateChanged(int state) {
// Code goes here
}
});
The notifyDataSetChanged causes the viewPager to call getItemPosition in the FragmentStatePagerAdapter class to work out if the page position has changed and it if need to re-create it in the a new position.
Then in FragmentStatePagerAdapter extended class I overrode getItemPosition with
#Override
public int getItemPosition(Object object) {
if (object instanceof LogFragment) {
LogFragment f = (LogFragment) object;
if (f != null) {
f.update();
}
} else if (object instanceof SummaryFragment){
SummaryFragment f = (SummaryFragment) object;
if (f != null) {
f.update();
}
} else {
return POSITION_UNCHANGED;
}
return super.getItemPosition(object);
}
This allowed me to get the Fragment object and then call the update method on it but still returning POSITION_UNCHANGED so the viewpager did not try and re-create the Fragments.
Then in the update method of the Fragment I get the listview adapter and update the data.
public void update(){
adapter.clear();
adapter.addAll(datasource.getData());
}
Thus the more costly getting and display the dynamic data is only done when the page is actually display, NOT when it is pre-created by viewPager (because that pre-created view would be old out of date data anyway and would need to be updated)
There is one downside to this approach, the screen shown during the swipe is still the old data (either empty or data from when that page was last updated), this was acceptable to me.
I have four swipeable fragments on an activity in tabbed view mode.Each fragment should show JSON data from an API.
There are two problems
The first problem is that only in first fragment I am able to get the data.
And the second problem is that when swiping to next fragment I lost the data received in the first fragment.
I have four layouts for each of the fragment.
I am using custom listview to show the data.
*After creating fragment and getting theJSON data I am using custom list view to show it in the first fragment using aeroFirst method as below but I am not able to do the same in the other fragment. Creating fragments,JSON parsing and aeroFirst method are all in MainActivityclass *
snip of the code
private void aeroFirst(List<Details> mList) {
// layout = (LinearLayout) findViewById(R.id.linear);
for (Details bean : details) {
final String urlChar = bean.getUrl();
if (bean.getResType().equals("Videos"))
{
ListView listView = (ListView) findViewById(R.id.list);
CustomListViewAdapter adapter = new CustomListViewAdapter(this,
R.layout.list_item, mList);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(getApplicationContext(),
"Click ListItem Number " + urlChar, Toast.LENGTH_SHORT)
.show();
There must be an activity behind all four fragments, right? Store the values in that activity so that all four fragments can access the JSONs. This might not be the best in style, but the other solution involves saving it into shared preference or similar approach.
Another way to do this is to load everything in the activity, and then put the JSONs into arguments as bundles for each fragment.
I have a very simple ToDo list app with multiple categories the user can add an item to. The architecture looks something like this:
http://i.imgur.com/WA3dtcU.png
I am using a ViewPager and each view is a fragment that I instantiate in the ViewPagerAdapater:
public Fragment getItem(int i) {
ToDoCategoryModel cat = categories().get(i);
ToDoCategoryView view = ToDoCategoryView.newInstance(cat);
return view;
}
Within each of these category the user can add new Item Fragments to a Linear Layout.
A very strange thing happens though. I start off on Category 1 (C1) Which currently has 1 Item in the list. Then scroll to C2 then scroll again to C3. Then I scroll back to C2. The ViewPager asks my ViewPagerAdapter to re-create C1 as it was destroyed after having scrolled 2 pages away from it. This is fine, I just create a brand new fragment (as the code above shows). However, when I now scroll back to C1, instead of 1 Item being shown (as you would expect) there are 2 items shown. They are a duplicate of each other.
The code I use to add the Item fragment to the Category fragment is:
public void addToDoItem(ToDoItemModel model) {
ToDoItemView newItem = ToDoItemView.newInstance(model, this);
getChildFragmentManager().beginTransaction().add(_linearLayout_items.getId(), newItem, "sameTag"+count++).commit();
}
If I change this code to add new TextView instead of fragments:
public void addToDoItem(ToDoItemModel model) {
ToDoItemView newItem = ToDoItemView.newInstance(model, this);
TextView tv = new TextView(getActivity());
tv.setText(newItem.getModel().getName());
_linearLayout_items.addView(tv);
}
It works fine! There are no duplicate items shown.
So what is wrong with adding fragments dynamically using the child fragment manager within a view pager?
Thanks.
As in, can the ELEMENTS of a ListView be Fragments. I know that you can assign a TextView XML to a ListView to change the way it looks, but can you add Fragments into a ListView.
For instance: I have a Fragment. The XML for said Fragment contains an ImageView, a couple of large-style TextViews, and a small-style TextView. The Fragment class code receives a Bundle, then based on the contents populates the TextViews and ImageView accordingly. Both the Fragment XML and the Fragment code work without issue
(I can display an individual Fragment just fine). I have a FragmentActivity in which I want to display the aforementioned list of Fragments. Here is the code I'm using to try to populate the ListView inside of the FragmentActivity's View:
ArrayList<Fragment> fragList = new ArrayList<Fragment>();
Fragment fragment = Fragment.instantiate(this, TileItem.class.getName());
Bundle bundle = new Bundle();
bundle.putInt("key", 0);
fragment.setArguments(bundle);
fragList.add(fragment);
ArrayAdapter<Fragment> adapter = new ArrayAdapter<Fragment>(this, R.layout.tile_item, fragList);
listItems.setAdapter(adapter);
Here's my mode of thinking on this. I make an ArrayList of Fragments to hold all of my instantiated Views. I then create a Fragment, create a Bundle, add data to the Bundle (so that the Fragment can marshal data into it's Views correctly), add the Bundle to the Fragment, then finally add the Fragment to the ArrayList. After that, I make an ArrayAdapter, add the element layout I want to use, and the list of Fragments I've made; then set the ListView to read from my adapter.
Anyone running this code will likely get the NPE # instantiating the ArrayAdapter. What gives? Is this even possible? Before I keep racking my brain on this can someone tell me if I'm just wasting my time? Is there a better way? I've been thinking of using a ScrollView, but so much of the functionality of a ListView would need to re-implemented and I hate-hate-hate reinventing the wheel when it's not necessary.
Thanks to anyone reading, and especially thank you for your thoughts if you decide to leave them. I've tried searching around for an established answer to this but all I seem to find are questions/web pages concerning using a ListView INSIDE of a Fragment; not using Fragments AS THE ELEMENTS of a ListView
Edit: I took the suggestions below and started investigating more. From the way things appear I should be able to use a custom adapter that inflates fragments instead of just flat out building from XML (for lack of a better way to describe the process) However, my current implementation is throwing an NPE when trying to set the adapter.
Here is my custom adapter code (shortened for brevity):
public class AdapterItem extends ArrayAdapter<Fragment> {
Context c;
List<Fragment> f;
public AdapterItem(Context c, List<Fragment> f) {
super(c, R.layout.tile_item, f);
this.c = c;
this.f = f;
}
#Override
public View getView(int pos, View v, ViewGroup vg) {
LayoutInflater i = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return i.inflate(R.layout.tile_item, vg, false);
}
}
and here is how I'm implementing it:
ArrayList<Fragment> fragList = new ArrayList<Fragment>();
Fragment fragment = Fragment.instantiate(this, TileItem.class.getName());
Bundle bundle = new Bundle();
bundle.putInt("key", 0);
fragment.setArguments(bundle);
fragList.add(fragment);
AdapterItem adapter = new AdapterItem(this, fragList);
adapter.add(fragment);
listItems.setAdapter(adapter);
So it's been a few days and I'm pretty sure this thread has been buried. However, I thought I would add one last update just in case someone wants to try this and a google search brings them here. So in my implementation I'm getting an NPE when the ListView is given the adapter. It doesn't take a rocket surgeon to figure out that it's certainly the adapter and not the ListView throwing the error. For the life of me I can't figure out why though...
At any rate, I think I have some idea though. First, a little back story: A while back I was trying to make FragmentTransactions inside of a FragmentDialog. Everytime I attempted to do so, I would get an NPE. Eventually, through much research, I discovered that the reason pertained to the way that Fragments are instanced. When a Fragment is called it needs the context from it's parent. Since a Dialog's parent is the Activity that started it, the Dialog itself didn't meet the criteria necessary. I believe, that when attempting to add fragments to a ListView, this is also the case. Since the ListView doesn't meet the agreement with instancing a Fragment it throws the NPE and thus, leaves me hanging and going back to conventions. D#mn...I had really hoped I would be able to do this. Using Fragments instead of simple XML would have made it so much easier to organize/search through the list. Oh well... guess it can't be done in case anyone is wondering.
I'd say this is not possible to do as putting a fragment in a ListView would mean the fragment can be multiplied across multiple containers. When you use the FragmentManager to create a fragment, it is tagged with an identifier, making it simple to reload and rearrange on orientation and other configuration changes. It also encourages uses across multiple device configs.
A Fragment is really a subset of an Activity. Would you ever have an Activity as part of a list? Definitely not (should be the answer!)!!!
Moreover, it is not very useful to attach() and detach() a fragment continuously as they move in and out of view (cells get recycled). These are all expensive operations that a ListView shouldn't deal with. Lists should scroll quickly.
From the conversation on the comments, I can see you want to achieve nice code with a good separation of view setup code and adapter in the Activity. Do so with either:
Override the View class and do your custom drawing and setup there.
Create a new class, in which you supply a context and data set required for it to get you back the view a list needs to show - this is what I usually do.
Have a Utils class to build your video elsewhere (silly).
Just don't use Fragments in Lists. Not the use case they are aiming for. HTH.
It turns out that you can create a ListView where each item in the listView is a Fragment. The trick is wrapping the Fragment in a FrameLayout.
UPDATE 9/16/2014
Even though it is possible to create a ListView that contain Fragments, it doesn't look like it's a good idea. This seems to definitely be a corner case in the Android world and there be dragons. For a simple fragment like the one in the example below everything works beautifully, but if you have a complex project with a lot going on in it then this is probably not the way to go. My new approach is to pull all of the GUI related code into a View that extends FrameLayout, and insert that into a the ListView -- this works MUCH BETTER and is more in line with how Android expects to be used. If you need the functionality of a Fragment in other parts of your code, you can simply use this new View there too.
Back to the original answer...
I've added a new ManyFragments example to my AnDevCon 14 Fragments example app if you want to try it out. Essentially it comes down the the BaseAdapter, which in my example looks like this:
BaseAdapter adapter = new BaseAdapter() {
#Override public int getCount() { return 10000; }
#Override public Object getItem(int i) { return new Integer(i); }
#Override public long getItemId(int i) { return i; }
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
if (view!=null){
ManyListItemFragment fragment = (ManyListItemFragment) view.getTag();
fragment.setCount(i);
} else {
FrameLayout layout = new FrameLayout(getActivity());
layout.setLayoutParams(frameLayoutParams);
int id = generateViewId();
layout.setId(id);
ManyListItemFragment fragment = new ManyListItemFragment();
fragment.setCount(i);
getChildFragmentManager()
.beginTransaction()
.replace(id,fragment)
.commit();
view = layout;
view.setTag(fragment);
}
return view;
}
};
In case you're curious here's generateViewId():
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static int generateViewId() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
} else {
return View.generateViewId();
}
}
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
You don't need to use Fragments.
Write a custom ViewAdapter and have it inflate a more complex layout (or maybe several more complex layouts if you need to get really fancy) then populate the fields of the layout as necessary.
[Aside: to the people who answered in comments -- please use answers rather than comments if you are actually answering the question! If only because you get more reputation points that way!]